Compare commits

...

5 Commits

Author SHA1 Message Date
jj.wang 5ea931f24d Design: lean LXC dispatcher with remote-only terminals (P1-6 generic remote launch) 2026-10-21 07:41:07 +00:00
jj.wang a505c138e0 smoke: --labels to route the smoke item (e.g. model:sol) 2026-10-21 07:27:27 +00:00
jj.wang bcc5fd44e7 Cron entry script for oregon (tick every 2 minutes under exec --secret-env) 2026-10-21 07:19:39 +00:00
jj.wang 1ae44eb705 Allow concurrency 0 to pause a terminal
A paused terminal gets no new runs and is not probed; in-flight runs still finish.
2026-10-21 07:17:29 +00:00
jj.wang b17b1f1587 Live config for oregon: GWENT tenant, win10 codex + oregon claude-code
codex is not installed on oregon, so oregon only offers claude-code. The codex
resume template passes the sandbox as a -c override (exec resume takes no -s).
2026-10-21 07:17:14 +00:00
8 changed files with 113 additions and 2 deletions
+4
View File
@@ -116,6 +116,9 @@ def cmd_smoke(cfg, a):
body = {"name": "agentd smoke %s %s" % (a.terminal, time.strftime("%Y-%m-%d %H:%M")), body = {"name": "agentd smoke %s %s" % (a.terminal, time.strftime("%Y-%m-%d %H:%M")),
"description_html": SMOKE_BRIEF, "state": pl.states(p["id"])[p["states"]["todo"]], "description_html": SMOKE_BRIEF, "state": pl.states(p["id"])[p["states"]["todo"]],
"assignees": [pl.members()[user]]} "assignees": [pl.members()[user]]}
if a.labels:
labels = pl.labels(p["id"])
body["labels"] = [labels[x] for x in a.labels.split(",")]
issue = pl.create_issue(p["id"], body) issue = pl.create_issue(p["id"], body)
key = "%s-%s" % (p.get("key_prefix", a.project), issue["sequence_id"]) key = "%s-%s" % (p.get("key_prefix", a.project), issue["sequence_id"])
print("created %s; ticking every %ss (timeout %s min)" % (key, a.interval, a.timeout)) print("created %s; ticking every %ss (timeout %s min)" % (key, a.interval, a.timeout))
@@ -195,6 +198,7 @@ def parser():
s.add_argument("terminal") s.add_argument("terminal")
s.add_argument("--interval", type=int, default=30) s.add_argument("--interval", type=int, default=30)
s.add_argument("--timeout", type=int, default=30, help="minutes") s.add_argument("--timeout", type=int, default=30, help="minutes")
s.add_argument("--labels", default="", help="comma-separated Plane labels, e.g. model:sol")
s.add_argument("--as", dest="as_") s.add_argument("--as", dest="as_")
s.set_defaults(fn=cmd_smoke) s.set_defaults(fn=cmd_smoke)
sub.add_parser("check-config", help="validate the config and list token env vars").set_defaults(fn=cmd_check_config) sub.add_parser("check-config", help="validate the config and list token env vars").set_defaults(fn=cmd_check_config)
+82
View File
@@ -0,0 +1,82 @@
# plane-agentd live configuration on oregon (phase 1). No secrets: token_key values are Vault key names; the tokens
# are injected by `agent-access.py exec --secret-env KEY=KEY` (see cron/agentd-tick.sh and docs/runbook.md).
plane: {base_url: https://plane.kappiee.com, workspace: kappiee}
vault: {client: ~/.local/share/server-access-client/agent-access.py, config: https://nas.tail9531b.ts.net:8443/identity/agent.json}
agentd:
state_dir: ~/.gwent-board/agentd # JSON state, agentd.log, heartbeat.json, local run dirs (runs/)
helpers_dir: /home/ubuntu/Documents/wjj/project/gwent-design/tools # win10-ps.sh / win10-put.sh
interval: 120
max_rounds_per_thread: 8
comment_max_chars: 6000
cli_terminal: oregon
terminals:
win10:
transport: warpgate-ssh
target: win10
os: windows
launch: schtasks
repo: 'C:\Users\Administrator\Documents\GwentAssistantProject\gwent'
run_root: 'C:\Users\Administrator\Documents\GwentAssistantProject\gwent\runtime\agentd'
bins: {codex: 'C:\Users\Administrator\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe'}
plane_user: window10
token_key: PLANE_WINDOW10_API_TOKEN
concurrency: 1
busy_processes: [codex] # do not start while a codex.exe agentd did not start is running
capabilities: [android, game]
executors: [codex]
oregon:
transport: local
os: linux
launch: subprocess
repo: /home/ubuntu/Documents/wjj/project/gwent
plane_user: oregon
token_key: PLANE_OREGON_API_TOKEN
concurrency: 1 # one shared worktree: no parallel edits
bins: {claude: /home/ubuntu/.local/bin/claude} # cron has a minimal PATH; codex is not installed on oregon
executors: [claude-code]
executors:
codex:
adapter: codex
run: "codex exec --json -C '{repo}' -s workspace-write -m {model} -c 'model_reasoning_effort=\"{effort}\"' {tier} -o '{out}' -"
# `exec resume` takes no -C/-s: the runner already cd's into the repo, the sandbox goes in as a -c override
resume: "codex exec resume {thread} --json -m {model} -c 'model_reasoning_effort=\"{effort}\"' -c 'sandbox_mode=\"workspace-write\"' {tier} -o '{out}' -"
tier_arg: "-c 'service_tier=\"{tier}\"'"
thread_from: events.jsonl:thread.started.thread_id
last_message: '{out}'
models: [gpt-6.1-sol, gpt-6-astra]
default_effort: medium
claude-code:
adapter: claude-code
run: "claude -p --model {model} --output-format json --permission-mode {perm}"
resume: "claude -p --resume {thread} --model {model} --output-format json --permission-mode {perm}"
permission_mode: acceptEdits
thread_from: json:session_id
last_message: json:result
models: [sonnet, opus, haiku]
projects:
GWENT:
id: 79cb5a60-aa4f-4138-9864-1dd70426e665
key_prefix: GW
states: {backlog: Backlog, todo: Todo, running: In Progress, waiting: Waiting, review: Review, done: Done}
reviewer: oregon
answerer: oregon
protocol_file: /home/ubuntu/Documents/wjj/project/gwent/design/board-protocol.md
routing:
by_assignee: {window10: win10, oregon: oregon}
by_label:
'model:astra': {model: gpt-6-astra, effort: high}
'model:sol': {model: gpt-6.1-sol, tier: fast}
'effort:high': {effort: high}
'needs-game': {serialize_group: game}
default_executor: {win10: codex, oregon: claude-code}
retry:
transient_max: 3
backoff_minutes: [2, 5, 10]
timeout_hours: 4
max_rounds_per_thread: 8
max_progress_rounds: 12
# notify (ntfy) is phase 3: left out on purpose
+1 -1
View File
@@ -53,7 +53,7 @@
"repo": {"type": "string"}, "repo": {"type": "string"},
"plane_user": {"type": "string"}, "plane_user": {"type": "string"},
"token_key": {"type": "string"}, "token_key": {"type": "string"},
"concurrency": {"type": "integer", "minimum": 1}, "concurrency": {"type": "integer", "minimum": 0},
"capabilities": {"$ref": "#/definitions/strings"}, "capabilities": {"$ref": "#/definitions/strings"},
"executors": {"type": "array", "items": {"type": "string"}, "minItems": 1}, "executors": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"bins": {"type": "object", "additionalProperties": {"type": "string"}}, "bins": {"type": "object", "additionalProperties": {"type": "string"}},
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# cron entry (*/2): one agentd tick under server-access exec (Plane tokens in the process env only), with a lock.
# The outer flock skips a tick while the previous one still runs; agentd also holds its own state lock.
exec flock -n /home/ubuntu/.gwent-board/agentd.lock \
timeout 900 python3 -I /home/ubuntu/.local/share/server-access-client/agent-access.py \
--config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
--secret-env PLANE_OREGON_API_TOKEN=PLANE_OREGON_API_TOKEN \
--secret-env PLANE_WINDOW10_API_TOKEN=PLANE_WINDOW10_API_TOKEN --timeout 850 -- \
python3 /home/ubuntu/Documents/wjj/project/plane-agentd/cli/agentd tick --once >> /home/ubuntu/.gwent-board/agentd-cron.log 2>&1
+2 -1
View File
@@ -94,4 +94,5 @@ docs/ design.md executor-contract.md runbook.md
## 9. 部署位置 ## 9. 部署位置
- Plane VM(plane-01)只跑 Plane:它是设备化节点(固定版本、每日备份停写路径),不放 agentd。 - Plane VM(plane-01)只跑 Plane:它是设备化节点(固定版本、每日备份停写路径),不放 agentd。
- 阶段 1–2:agentd 跑在 oregon(已有 Vault 客户端、Warpgate 中继脚本、Claude Code、现有 worker 状态),cron 拉起;配置页面同进程,只监听 tailnet 地址。 - 阶段 1–2:agentd 跑在 oregon(已有 Vault 客户端、Warpgate 中继脚本、Claude Code、现有 worker 状态),cron 拉起;配置页面同进程,只监听 tailnet 地址。
- 阶段 3 起:迁到 PVE 上的专用小节点 `agentd-01`(1–2 vCPU / 2 GB,tailnet + 内网 10.77.10.0/24,与 Plane/Temporal 同网段,webhook 走内网),装 Vault 客户端、中继脚本、Claude Code 与 Codex CLI,使其同时也是一个执行终端(跑核对/验收类任务)。对外访问经 Warpgate HTTP target,复用既有登录。 - 阶段 3 起:迁到 PVE 上的精简 LXC `agentd-01`(1 vCPU / 1–2 GB,tailnet + 内网 10.77.10.0/24,与 Plane/Temporal 同网段,webhook 走内网)。**LXC 上不装任何 agent CLI**,只有 Python 3.10+、PyYAML、Agent Vault 客户端、ssh 客户端;所有终端(win10、oregon、mac…)一律远程启动(`transport: warpgate-ssh`),LXC 只负责调度、回帖、通知。对外访问经 Warpgate HTTP target,复用既有登录。
- 远程启动的通用实现(P1-6):Windows 用计划任务(已实现);Linux/macOS 经 Warpgate SSH 上传 `run.sh`,`setsid nohup` 脱离会话,轮询 `exit` 文件取回 `last.md`/线程 ID;中继目标名由终端配置给出,不写死。
+1
View File
@@ -40,6 +40,7 @@ cli/agentd comment GWENT 12 reply.md # 或 - 读 stdin
cli/agentd state GWENT 12 Todo # Plane 状态名或配置键(todo/review…) cli/agentd state GWENT 12 Todo # Plane 状态名或配置键(todo/review…)
cli/agentd assign GWENT 12 window10 cli/agentd assign GWENT 12 window10
cli/agentd smoke GWENT oregon # 建一个要求回复 "[review] ok" 的工作项,tick 到结束,PASS 则移到 Done cli/agentd smoke GWENT oregon # 建一个要求回复 "[review] ok" 的工作项,tick 到结束,PASS 则移到 Done
cli/agentd smoke GWENT win10 --labels model:sol # --labels 给冒烟项加标签(路由到模型/档位)
``` ```
CLI 以 `--as <terminal>` / `agentd.cli_terminal` / 第一个有 token 的终端的身份操作。 CLI 以 `--as <terminal>` / `agentd.cli_terminal` / 第一个有 token 的终端的身份操作。
+6
View File
@@ -45,6 +45,12 @@ class ConfigTest(unittest.TestCase):
d["terminals"]["oregon"]["concurrency"] = "two" d["terminals"]["oregon"]["concurrency"] = "two"
self.check_both(d, "concurrency") self.check_both(d, "concurrency")
def test_concurrency_zero_is_valid(self): # 0 = terminal paused (nothing new is dispatched)
d = config_data()
d["terminals"]["win10"]["concurrency"] = 0
self.assertEqual(config.validate(d), [])
self.assertEqual(config.mini_validate(d, SCHEMA), [])
def test_semantic_routing_to_unknown_terminal(self): def test_semantic_routing_to_unknown_terminal(self):
d = config_data() d = config_data()
d["projects"]["GWENT"]["routing"]["by_assignee"]["mac"] = "mac" d["projects"]["GWENT"]["routing"]["by_assignee"]["mac"] = "mac"
+8
View File
@@ -160,6 +160,14 @@ class CoreTest(unittest.TestCase):
self.assertEqual(started, ["GW-1-1", "GW-3-1"]) # 2 slots; GW-2 waits for the game group self.assertEqual(started, ["GW-1-1", "GW-3-1"]) # 2 slots; GW-2 waits for the game group
self.assertEqual([self.p.state_of(x) for x in (a, b, c, d)], ["In Progress", "Todo", "In Progress", "Todo"]) self.assertEqual([self.p.state_of(x) for x in (a, b, c, d)], ["In Progress", "Todo", "In Progress", "Todo"])
def test_concurrency_zero_pauses_terminal(self):
self.h.cfg.terminals["win10"]["concurrency"] = 0
w = self.p.add_issue(1)
o = self.p.add_issue(2, assignee="oregon")
self.h.tick()
self.assertNotIn(("win10", "codex"), self.h.execs) # not even probed (busy_processes)
self.assertEqual((self.p.state_of(w), self.p.state_of(o)), ("Todo", "In Progress"))
def test_label_routing(self): def test_label_routing(self):
self.p.add_issue(1, labels=["model:astra"]) self.p.add_issue(1, labels=["model:astra"])
self.p.add_issue(2, assignee="oregon", labels=["cap:gpu"]) # oregon lacks the capability self.p.add_issue(2, assignee="oregon", labels=["cap:gpu"]) # oregon lacks the capability