wq-pilot 0.2.2__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.
- wq/__init__.py +1 -0
- wq/__main__.py +4 -0
- wq/account.py +67 -0
- wq/adapters/__init__.py +0 -0
- wq/adapters/base.py +30 -0
- wq/adapters/brain.py +43 -0
- wq/adapters/manual.py +43 -0
- wq/assets/__init__.py +6 -0
- wq/assets/autopilot-policy.example.json +49 -0
- wq/assets/config.example.json +122 -0
- wq/assets/docker_provider.py +91 -0
- wq/assets/profiles.example.json +173 -0
- wq/assets/provider_entry.py +23 -0
- wq/assets/provider_login.py +30 -0
- wq/autopilot.py +936 -0
- wq/brain_client.py +175 -0
- wq/brain_jobs.py +223 -0
- wq/brain_submission.py +295 -0
- wq/catalog.py +195 -0
- wq/cli.py +884 -0
- wq/config.py +186 -0
- wq/contracts.py +162 -0
- wq/db.py +169 -0
- wq/dedup.py +39 -0
- wq/desktop.py +222 -0
- wq/desktop_control.py +237 -0
- wq/errors.py +41 -0
- wq/evidence_gate.py +430 -0
- wq/exporter.py +59 -0
- wq/feedback.py +578 -0
- wq/history_research.py +190 -0
- wq/i18n.py +83 -0
- wq/importer.py +97 -0
- wq/onboard.py +250 -0
- wq/pnl.py +62 -0
- wq/progress.py +40 -0
- wq/provider_runtime.py +178 -0
- wq/providers.py +202 -0
- wq/reconcile.py +85 -0
- wq/refinement.py +47 -0
- wq/report.py +118 -0
- wq/research_dsl.py +161 -0
- wq/research_gate.py +55 -0
- wq/routing.py +337 -0
- wq/runner.py +203 -0
- wq/setup_linux.py +73 -0
- wq/setup_local.py +86 -0
- wq/store.py +421 -0
- wq/task_view.py +272 -0
- wq/usage.py +181 -0
- wq/util.py +86 -0
- wq/windows_setup.py +95 -0
- wq/workflow.py +199 -0
- wq/wrappers/__init__.py +0 -0
- wq/wrappers/agent.py +362 -0
- wq_pilot-0.2.2.dist-info/METADATA +130 -0
- wq_pilot-0.2.2.dist-info/RECORD +62 -0
- wq_pilot-0.2.2.dist-info/WHEEL +5 -0
- wq_pilot-0.2.2.dist-info/entry_points.txt +2 -0
- wq_pilot-0.2.2.dist-info/licenses/LICENSE +202 -0
- wq_pilot-0.2.2.dist-info/licenses/NOTICE.md +9 -0
- wq_pilot-0.2.2.dist-info/top_level.txt +1 -0
wq/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
wq/__main__.py
ADDED
wq/account.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""只读账户快照:等级、pyramid 乘数与数量、连击。社区工具(wqb-agent-tools MIT)记录的端点,
|
|
2
|
+
本账号可用性未核验;任一端点 4xx 即记录为不可用并继续,不重试、不枚举其他接口。快照写私有目录,不含凭证。"""
|
|
3
|
+
import os
|
|
4
|
+
from . import util
|
|
5
|
+
|
|
6
|
+
# 端点 → 快照键。用途:判断是否存在本账号可用的高乘数 pyramid,是否改变研究设置由人工另行决定。
|
|
7
|
+
ENDPOINTS = (('/users/self', 'profile'),
|
|
8
|
+
('/users/self/activities/pyramid-multipliers', 'pyramid_multipliers'),
|
|
9
|
+
('/users/self/activities/pyramid-alphas', 'pyramid_alphas'),
|
|
10
|
+
('/users/self/streak', 'streak'))
|
|
11
|
+
PROFILE_KEYS = ('id', 'level', 'geniusLevel', 'points', 'permissions', 'stage', 'dateCreated')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def snapshot(get, client, cfg):
|
|
15
|
+
"""get(client, cfg, path) -> (status, headers, data);每个端点最多一次请求。"""
|
|
16
|
+
from .errors import AdapterError
|
|
17
|
+
out = {'schema': 'wq.account-snapshot/v1', 'queried_at': util.now_iso(), 'endpoints': {}}
|
|
18
|
+
for path, key in ENDPOINTS:
|
|
19
|
+
try:
|
|
20
|
+
status, _, data = get(client, cfg, path)
|
|
21
|
+
out['endpoints'][key] = {'path': path, 'status': status, 'data': data}
|
|
22
|
+
except AdapterError as exc:
|
|
23
|
+
out['endpoints'][key] = {'path': path, 'status': None, 'error': str(exc), 'kind': exc.kind}
|
|
24
|
+
if exc.kind in (AdapterError.NETWORK, AdapterError.RATE_LIMIT, AdapterError.AUTH):
|
|
25
|
+
break # 网络/限流/认证问题不继续打其余端点
|
|
26
|
+
profile = (out['endpoints'].get('profile') or {}).get('data') or {}
|
|
27
|
+
if isinstance(profile, dict):
|
|
28
|
+
# 只保留等级/权限类字段;邮箱、姓名等身份信息不落盘。
|
|
29
|
+
out['endpoints']['profile']['data'] = {k: profile.get(k) for k in PROFILE_KEYS if k in profile}
|
|
30
|
+
path = os.path.join(cfg.private_dir, 'brain-account', 'snapshot-' + out['queried_at'].replace(':', '').replace('+', 'Z') + '.json')
|
|
31
|
+
os.makedirs(os.path.dirname(path), exist_ok=True, mode=0o700)
|
|
32
|
+
util.write_json(path, out)
|
|
33
|
+
os.chmod(path, 0o600)
|
|
34
|
+
out['saved_to'] = path
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _rows(data):
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
for key in ('pyramids', 'results'):
|
|
41
|
+
if isinstance(data.get(key), list): return data[key]
|
|
42
|
+
return data if isinstance(data, list) else []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def summary(snap):
|
|
46
|
+
"""人读摘要:可用端点、pyramid 乘数排序(高→低)与本账号各 pyramid 的 alpha 数、连击。"""
|
|
47
|
+
ep = snap['endpoints']
|
|
48
|
+
profile = (ep.get('profile') or {}).get('data') or {}
|
|
49
|
+
mult = {}
|
|
50
|
+
for row in _rows((ep.get('pyramid_multipliers') or {}).get('data')):
|
|
51
|
+
if not isinstance(row, dict): continue
|
|
52
|
+
cat = row.get('category'); cat = cat.get('id') if isinstance(cat, dict) else cat
|
|
53
|
+
mult[(str(cat), str(row.get('region')), str(row.get('delay')))] = row.get('multiplier')
|
|
54
|
+
counts = {}
|
|
55
|
+
for row in _rows((ep.get('pyramid_alphas') or {}).get('data')):
|
|
56
|
+
if not isinstance(row, dict): continue
|
|
57
|
+
cat = row.get('category'); cat = cat.get('id') if isinstance(cat, dict) else cat
|
|
58
|
+
counts[(str(cat), str(row.get('region')), str(row.get('delay')))] = row.get('alphaCount')
|
|
59
|
+
pyramids = [{'category': k[0], 'region': k[1], 'delay': k[2], 'multiplier': mult.get(k), 'alpha_count': counts.get(k)}
|
|
60
|
+
for k in sorted(set(mult) | set(counts), key=lambda k: (-(mult.get(k) or 0), k))]
|
|
61
|
+
return {'saved_to': snap.get('saved_to'),
|
|
62
|
+
'available': {k: (v.get('status') if v.get('status') else 'unavailable: ' + str(v.get('error'))) for k, v in ep.items()},
|
|
63
|
+
'level': {k: profile.get(k) for k in ('level', 'geniusLevel', 'points', 'stage') if k in profile},
|
|
64
|
+
'permissions': profile.get('permissions'),
|
|
65
|
+
'pyramids': pyramids,
|
|
66
|
+
'streak': (ep.get('streak') or {}).get('data'),
|
|
67
|
+
'note': '只读快照;乘数是否适用于本账号积分/报酬以平台说明为准。改变研究设置(region/delay)不由本命令自动触发。'}
|
wq/adapters/__init__.py
ADDED
|
File without changes
|
wq/adapters/base.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Adapter 协议与错误分类。真实 adapter 必须遵守:
|
|
2
|
+
|
|
3
|
+
- 401/403 → AdapterError(AUTH),上层暂停并标记身份/权限问题;
|
|
4
|
+
- 429 → AdapterError(RATE_LIMIT, retry_after=官方 Retry-After),上层退避且总等待有上限;
|
|
5
|
+
- 超时/连接中断 → AdapterError(UNKNOWN_REMOTE):远端可能已接受,先对账,绝不盲目重发;
|
|
6
|
+
- 没有官方幂等键就不要伪造;dedup_key 只做本地去重,不构成远端幂等。
|
|
7
|
+
|
|
8
|
+
模拟通过 / 提交接受 / 最终有效 / 报酬资格 / 实际到账是五个不同状态。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ..errors import AdapterError # re-export
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimAdapter:
|
|
16
|
+
name = "abstract"
|
|
17
|
+
|
|
18
|
+
def status(self) -> dict:
|
|
19
|
+
return {"adapter": self.name}
|
|
20
|
+
|
|
21
|
+
def simulate(self, payload: dict) -> dict:
|
|
22
|
+
"""返回 {remote_id: str} 回执。抛 AdapterError 表达失败类别。"""
|
|
23
|
+
raise AdapterError(AdapterError.NOT_IMPLEMENTED, "simulate 未实现")
|
|
24
|
+
|
|
25
|
+
def submit(self, payload: dict) -> dict:
|
|
26
|
+
raise AdapterError(AdapterError.NOT_IMPLEMENTED, "submit 未实现")
|
|
27
|
+
|
|
28
|
+
def reconcile(self, unknown: dict) -> dict:
|
|
29
|
+
"""对账 unknown 任务/模拟/提交。返回每项的可核实结论或仍需人工。"""
|
|
30
|
+
raise AdapterError(AdapterError.NOT_IMPLEMENTED, "reconcile 未实现")
|
wq/adapters/brain.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""旧 simulation/submission adapter 保留为阻断入口。
|
|
2
|
+
|
|
3
|
+
真实单次模拟使用 brain_jobs 的 brain_simulation 状态机;此类不转发,
|
|
4
|
+
避免旧任务绕过新路径的证据、幂等与授权门禁。自动提交未实现。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..errors import AdapterError
|
|
9
|
+
from .base import SimAdapter
|
|
10
|
+
|
|
11
|
+
_REASON = "旧adapter不发网络请求;真实模拟请用 wq brain enqueue,提交仍需本人处理"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BrainAPIAdapter(SimAdapter):
|
|
15
|
+
name = "brain-api"
|
|
16
|
+
|
|
17
|
+
def __init__(self, endpoint: str | None = None, verified: bool = False):
|
|
18
|
+
self.endpoint = endpoint
|
|
19
|
+
self.verified = verified
|
|
20
|
+
|
|
21
|
+
def status(self) -> dict:
|
|
22
|
+
return {"adapter": self.name, "endpoint": self.endpoint,
|
|
23
|
+
"verified": self.verified, "state": "not_implemented", "reason": _REASON}
|
|
24
|
+
|
|
25
|
+
def _ni(self, op: str):
|
|
26
|
+
raise AdapterError(AdapterError.NOT_IMPLEMENTED, f"{op}: {_REASON}")
|
|
27
|
+
|
|
28
|
+
def simulate(self, payload: dict) -> dict:
|
|
29
|
+
self._ni("simulate")
|
|
30
|
+
|
|
31
|
+
def submit(self, payload: dict) -> dict:
|
|
32
|
+
self._ni("submit")
|
|
33
|
+
|
|
34
|
+
def reconcile(self, unknown: dict) -> dict:
|
|
35
|
+
self._ni("reconcile")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_adapter(cfg) -> SimAdapter:
|
|
39
|
+
from .manual import ManualAdapter
|
|
40
|
+
if cfg.adapter_mode() == "api":
|
|
41
|
+
return BrainAPIAdapter(endpoint=cfg.get("adapters", "brain", "endpoint"),
|
|
42
|
+
verified=bool(cfg.get("adapters", "brain", "verified")))
|
|
43
|
+
return ManualAdapter()
|
wq/adapters/manual.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""manual-import adapter:API 不可用时的默认模式。
|
|
2
|
+
|
|
3
|
+
模拟/提交由用户在 BRAIN 官方 UI 手工完成,结果经 `wq import-results` 入库。
|
|
4
|
+
这里不做任何网络动作;simulate/submit 一律 POLICY 拒绝并指明人工步骤。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..errors import AdapterError
|
|
9
|
+
from .base import SimAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ManualAdapter(SimAdapter):
|
|
13
|
+
name = "manual"
|
|
14
|
+
|
|
15
|
+
def status(self) -> dict:
|
|
16
|
+
return {"adapter": self.name,
|
|
17
|
+
"note": "人工模式:在 BRAIN UI 完成模拟后用 import-results 导入回执"}
|
|
18
|
+
|
|
19
|
+
def simulate(self, payload: dict) -> dict:
|
|
20
|
+
raise AdapterError(
|
|
21
|
+
AdapterError.POLICY,
|
|
22
|
+
"manual 模式不发请求:请在 BRAIN UI 运行该配置,再用 "
|
|
23
|
+
"`wq import-results <file> --real` 导入真实结果")
|
|
24
|
+
|
|
25
|
+
def submit(self, payload: dict) -> dict:
|
|
26
|
+
raise AdapterError(
|
|
27
|
+
AdapterError.POLICY,
|
|
28
|
+
"manual 模式不提交:提交需本人确认后在官方入口完成,再用对账/导入登记状态")
|
|
29
|
+
|
|
30
|
+
def reconcile(self, unknown: dict) -> dict:
|
|
31
|
+
# 离线可完成的部分:列出需要人工核实的清单,不编造远端状态。
|
|
32
|
+
items = []
|
|
33
|
+
for t in unknown.get("tasks", []):
|
|
34
|
+
items.append({"kind": "task", "id": t["task_id"],
|
|
35
|
+
"action": "在 BRAIN UI 核对该远端动作是否已被接受,"
|
|
36
|
+
"然后 `wq reconcile --resolve <task_id> --outcome accepted|lost`"})
|
|
37
|
+
for s in unknown.get("simulations", []):
|
|
38
|
+
items.append({"kind": "simulation", "id": s["sim_id"],
|
|
39
|
+
"action": "核对 remote_id 对应模拟的最终状态并重新导入"})
|
|
40
|
+
for s in unknown.get("submissions", []):
|
|
41
|
+
items.append({"kind": "submission", "id": s["submission_id"],
|
|
42
|
+
"action": "核对提交最终有效性(accepted≠final_valid)并更新状态"})
|
|
43
|
+
return {"adapter": self.name, "manual_actions": items}
|
wq/assets/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"scope": "exploratory_only_no_submission",
|
|
4
|
+
"verified_at": null,
|
|
5
|
+
"valid_until": null,
|
|
6
|
+
"source": "REPLACE_WITH_VERIFIED_SOURCE",
|
|
7
|
+
"bindings": {
|
|
8
|
+
"daily_return": {
|
|
9
|
+
"expression": "",
|
|
10
|
+
"fields": [],
|
|
11
|
+
"source": "",
|
|
12
|
+
"description": "Model-visible description: what the role measures and what it does NOT mean. Never put platform field IDs here.",
|
|
13
|
+
"cluster": "pv"
|
|
14
|
+
},
|
|
15
|
+
"activity_rank": {
|
|
16
|
+
"expression": "",
|
|
17
|
+
"fields": [],
|
|
18
|
+
"source": ""
|
|
19
|
+
},
|
|
20
|
+
"cashflow_strength": {
|
|
21
|
+
"expression": "",
|
|
22
|
+
"fields": [],
|
|
23
|
+
"source": ""
|
|
24
|
+
},
|
|
25
|
+
"market_cap_rank": {
|
|
26
|
+
"expression": "",
|
|
27
|
+
"fields": [],
|
|
28
|
+
"source": ""
|
|
29
|
+
},
|
|
30
|
+
"price_vwap_gap": {
|
|
31
|
+
"expression": "",
|
|
32
|
+
"fields": [],
|
|
33
|
+
"source": ""
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"evidence_files": [],
|
|
37
|
+
"settings": {},
|
|
38
|
+
"limits": "Only exploratory screens. No automatic submission. Register more roles with `wq policy add-role`; setting_variants are preregistered per cycle (max 2, decay/neutralization/truncation only).",
|
|
39
|
+
"setting_variants": [
|
|
40
|
+
{
|
|
41
|
+
"label": "decay8",
|
|
42
|
+
"decay": 8
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"label": "subindustry",
|
|
46
|
+
"neutralization": "SUBINDUSTRY"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"account_alias": "default",
|
|
3
|
+
"paths": {
|
|
4
|
+
"db": "var/wq.db",
|
|
5
|
+
"run_dir": "var/run",
|
|
6
|
+
"private_dir": "~/.worldquant-pilot"
|
|
7
|
+
},
|
|
8
|
+
"limits": {
|
|
9
|
+
"sims_per_week": 24,
|
|
10
|
+
"sim_concurrency": 1,
|
|
11
|
+
"grok_calls_per_week": 3,
|
|
12
|
+
"devin_tickets_per_week": 1,
|
|
13
|
+
"max_repair_attempts_per_incident": 2,
|
|
14
|
+
"families_max": 3,
|
|
15
|
+
"configs_per_family_max": 4,
|
|
16
|
+
"rate_limit_max_wait_s": 900
|
|
17
|
+
},
|
|
18
|
+
"budgets": {
|
|
19
|
+
"simulation": {
|
|
20
|
+
"enabled": false,
|
|
21
|
+
"remaining": null,
|
|
22
|
+
"unit": "unknown"
|
|
23
|
+
},
|
|
24
|
+
"grok": {
|
|
25
|
+
"enabled": false,
|
|
26
|
+
"remaining": null,
|
|
27
|
+
"unit": "unknown"
|
|
28
|
+
},
|
|
29
|
+
"devin": {
|
|
30
|
+
"enabled": false,
|
|
31
|
+
"remaining": null,
|
|
32
|
+
"unit": "unknown"
|
|
33
|
+
},
|
|
34
|
+
"cursor": {
|
|
35
|
+
"enabled": false,
|
|
36
|
+
"remaining": null,
|
|
37
|
+
"unit": "calls"
|
|
38
|
+
},
|
|
39
|
+
"zcode": {
|
|
40
|
+
"enabled": false,
|
|
41
|
+
"remaining": null,
|
|
42
|
+
"unit": "calls"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"adapters": {
|
|
46
|
+
"brain": {
|
|
47
|
+
"mode": "manual",
|
|
48
|
+
"endpoint": null,
|
|
49
|
+
"verified": false
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"models": {
|
|
53
|
+
"grok": {
|
|
54
|
+
"bin": "~/.grok/bin/grok",
|
|
55
|
+
"enabled": false,
|
|
56
|
+
"timeout_s": 900,
|
|
57
|
+
"workdir": null,
|
|
58
|
+
"extra_args": []
|
|
59
|
+
},
|
|
60
|
+
"devin": {
|
|
61
|
+
"bin": "~/.local/bin/devin",
|
|
62
|
+
"enabled": false,
|
|
63
|
+
"timeout_s": 1800,
|
|
64
|
+
"workdir": null,
|
|
65
|
+
"extra_args": []
|
|
66
|
+
},
|
|
67
|
+
"cursor": {
|
|
68
|
+
"enabled": false,
|
|
69
|
+
"timeout_s": 900
|
|
70
|
+
},
|
|
71
|
+
"zcode": {
|
|
72
|
+
"enabled": false,
|
|
73
|
+
"timeout_s": 900
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"debug_authorization": {
|
|
77
|
+
"enabled": false,
|
|
78
|
+
"starts_at": null,
|
|
79
|
+
"expires_at": null,
|
|
80
|
+
"agents": [],
|
|
81
|
+
"unlimited": false,
|
|
82
|
+
"evidence": ""
|
|
83
|
+
},
|
|
84
|
+
"brain_api": {
|
|
85
|
+
"enabled": false,
|
|
86
|
+
"authorized_until": null,
|
|
87
|
+
"max_posts_per_24h": 4,
|
|
88
|
+
"min_post_interval_s": 60,
|
|
89
|
+
"auto_login": false,
|
|
90
|
+
"auto_login_email": null,
|
|
91
|
+
"keychain_service": "com.worldquant.wq.brain"
|
|
92
|
+
},
|
|
93
|
+
"routing": {
|
|
94
|
+
"profiles_file": "config/profiles.json",
|
|
95
|
+
"work_root": "{runtime}/jobs",
|
|
96
|
+
"authorized_until": null
|
|
97
|
+
},
|
|
98
|
+
"autopilot": {
|
|
99
|
+
"enabled": false,
|
|
100
|
+
"policy_file": "config/autopilot-policy.json",
|
|
101
|
+
"interval_s": 3600,
|
|
102
|
+
"error_cooldown_s": 3600,
|
|
103
|
+
"max_cycles_per_day": 4,
|
|
104
|
+
"max_simulations_per_week": 24,
|
|
105
|
+
"max_cycles_total": null,
|
|
106
|
+
"sign_flip_rescue_sharpe": -0.8
|
|
107
|
+
},
|
|
108
|
+
"brain_submission": {
|
|
109
|
+
"enabled": false,
|
|
110
|
+
"authorized_until": null
|
|
111
|
+
},
|
|
112
|
+
"research_feedback": {
|
|
113
|
+
"enabled": false,
|
|
114
|
+
"max_combination_plans": 2,
|
|
115
|
+
"segment_rules": {
|
|
116
|
+
"min_sharpe": 1.25,
|
|
117
|
+
"min_fitness": 1.0,
|
|
118
|
+
"min_years": 3,
|
|
119
|
+
"max_negative_years": 0
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run one owned provider container, with bounded lifetime and no host credentials mount."""
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def command(config, provider, workdir, prompt, model):
|
|
15
|
+
work = Path(workdir).resolve()
|
|
16
|
+
jobs = Path(config['jobs_root']).resolve()
|
|
17
|
+
source = Path(prompt)
|
|
18
|
+
if not work.is_relative_to(jobs) or work == jobs or source.is_symlink():
|
|
19
|
+
raise ValueError('Only a single screened task directory may be mounted')
|
|
20
|
+
source = source.resolve()
|
|
21
|
+
if not source.is_relative_to(work) or not source.is_file():
|
|
22
|
+
raise ValueError('Prompt must be a regular file inside the task directory')
|
|
23
|
+
if any(p.is_symlink() for p in work.rglob('*')):
|
|
24
|
+
raise ValueError('Task directory contains symlinks')
|
|
25
|
+
if ',' in str(work):
|
|
26
|
+
raise ValueError('Docker mount source must not contain commas')
|
|
27
|
+
item = config['providers'][provider]
|
|
28
|
+
image, volume = item['image'], item['home_volume']
|
|
29
|
+
if not image or image.startswith('-') or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]+', volume):
|
|
30
|
+
raise ValueError('Invalid image or isolated home volume')
|
|
31
|
+
ttl = item.get('timeout_s', 900)
|
|
32
|
+
if type(ttl) is not int or not 1 <= ttl <= 3600:
|
|
33
|
+
raise ValueError('Container timeout must be 1..3600 seconds')
|
|
34
|
+
name = 'autowq-' + hashlib.sha256((str(work)+provider).encode()).hexdigest()[:24]
|
|
35
|
+
substitutions = {'{cwd}':'/work', '{prompt}':'/work/'+source.relative_to(work).as_posix(), '{model}':model}
|
|
36
|
+
argv = item['argv']
|
|
37
|
+
if not isinstance(argv,list) or not argv or not all(isinstance(x,str) for x in argv):
|
|
38
|
+
raise ValueError('Provider argv must be a nonempty string array')
|
|
39
|
+
for a,b in substitutions.items(): argv=[x.replace(a,b) for x in argv]
|
|
40
|
+
cmd = [config.get('docker_bin','docker'),'run','--rm','--init','--pull=never','--name',name,
|
|
41
|
+
'--label','autowq.managed=true','--read-only','--cap-drop=ALL',
|
|
42
|
+
'--security-opt=no-new-privileges','--pids-limit=256','--memory=2g','--cpus=2',
|
|
43
|
+
'--user',f'{os.getuid()}:{os.getgid()}',
|
|
44
|
+
'--tmpfs','/tmp:rw,nosuid,nodev,size=256m',
|
|
45
|
+
'--mount',f'type=bind,source={work},target=/work',
|
|
46
|
+
'--mount',f'type=volume,source={volume},target=/home/agent',
|
|
47
|
+
'--workdir','/work','--env','HOME=/home/agent',
|
|
48
|
+
'--entrypoint','/usr/bin/timeout',image,'--signal=TERM','--kill-after=5',str(ttl),*argv]
|
|
49
|
+
return cmd, name, ttl
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(config, provider, workdir, prompt, model=''):
|
|
53
|
+
argv, name, ttl = command(config,provider,workdir,prompt,model)
|
|
54
|
+
docker=argv[0]
|
|
55
|
+
# A surviving container belongs to the old attempt. Never replace/restart it.
|
|
56
|
+
existing=subprocess.run([docker,'container','inspect',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15)
|
|
57
|
+
if existing.returncode==0:
|
|
58
|
+
raise ValueError('Previous container exists; reconcile it before retrying')
|
|
59
|
+
proc=None
|
|
60
|
+
previous={}
|
|
61
|
+
def stop(signum, frame):
|
|
62
|
+
raise SystemExit(128+signum)
|
|
63
|
+
try:
|
|
64
|
+
for sig in (signal.SIGTERM,signal.SIGINT):previous[sig]=signal.signal(sig,stop)
|
|
65
|
+
proc=subprocess.Popen(argv,stdin=subprocess.DEVNULL)
|
|
66
|
+
try:return proc.wait(timeout=ttl+30)
|
|
67
|
+
except subprocess.TimeoutExpired:return 124
|
|
68
|
+
finally:
|
|
69
|
+
# Only the deterministic name owned by this newly launched attempt is removed.
|
|
70
|
+
if proc is not None:
|
|
71
|
+
try:subprocess.run([docker,'rm','-f',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=3)
|
|
72
|
+
except (OSError,subprocess.TimeoutExpired):pass
|
|
73
|
+
if proc.poll() is None:
|
|
74
|
+
proc.terminate()
|
|
75
|
+
try:proc.wait(timeout=1)
|
|
76
|
+
except subprocess.TimeoutExpired:proc.kill();proc.wait()
|
|
77
|
+
for sig,handler in previous.items():signal.signal(sig,handler)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
p=argparse.ArgumentParser(description=__doc__)
|
|
82
|
+
p.add_argument('--config',required=True)
|
|
83
|
+
p.add_argument('provider');p.add_argument('workdir');p.add_argument('prompt');p.add_argument('model',nargs='?',default='')
|
|
84
|
+
a=p.parse_args()
|
|
85
|
+
try:
|
|
86
|
+
config=json.loads(Path(a.config).read_text())
|
|
87
|
+
return run(config,a.provider,a.workdir,a.prompt,a.model)
|
|
88
|
+
except (OSError,ValueError,KeyError,subprocess.TimeoutExpired) as exc:
|
|
89
|
+
print('Container runner: '+str(exc),file=sys.stderr);return 2
|
|
90
|
+
|
|
91
|
+
if __name__=='__main__':sys.exit(main())
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
{
|
|
2
|
+
"default": "steady",
|
|
3
|
+
"providers": {
|
|
4
|
+
"grok": {
|
|
5
|
+
"label": "Grok Build",
|
|
6
|
+
"argv": [
|
|
7
|
+
"{launcher_dir}/grok",
|
|
8
|
+
"--cwd",
|
|
9
|
+
"{cwd}",
|
|
10
|
+
"--no-subagents",
|
|
11
|
+
"--max-turns",
|
|
12
|
+
"12",
|
|
13
|
+
"--output-format",
|
|
14
|
+
"json",
|
|
15
|
+
"--verbatim",
|
|
16
|
+
"--always-approve",
|
|
17
|
+
"--prompt-file",
|
|
18
|
+
"{prompt}"
|
|
19
|
+
],
|
|
20
|
+
"timeout_s": 900,
|
|
21
|
+
"terminal_protocol": "grok"
|
|
22
|
+
},
|
|
23
|
+
"devin": {
|
|
24
|
+
"label": "Devin",
|
|
25
|
+
"argv": [
|
|
26
|
+
"{launcher_dir}/devin",
|
|
27
|
+
"-p",
|
|
28
|
+
"--prompt-file",
|
|
29
|
+
"{prompt}",
|
|
30
|
+
"--permission-mode",
|
|
31
|
+
"dangerous",
|
|
32
|
+
"--respect-workspace-trust",
|
|
33
|
+
"false"
|
|
34
|
+
],
|
|
35
|
+
"timeout_s": 1800
|
|
36
|
+
},
|
|
37
|
+
"cursor": {
|
|
38
|
+
"label": "Cursor / Grok 4.6",
|
|
39
|
+
"argv": [
|
|
40
|
+
"{launcher_dir}/cursor",
|
|
41
|
+
"{cwd}",
|
|
42
|
+
"{prompt}",
|
|
43
|
+
"{model}"
|
|
44
|
+
],
|
|
45
|
+
"model": "cursor-grok-4.6-high",
|
|
46
|
+
"timeout_s": 900,
|
|
47
|
+
"terminal_protocol": "cursor"
|
|
48
|
+
},
|
|
49
|
+
"zcode": {
|
|
50
|
+
"label": "ZCode / Z.ai v1 Lite",
|
|
51
|
+
"argv": [
|
|
52
|
+
"{launcher_dir}/zcode",
|
|
53
|
+
"{cwd}",
|
|
54
|
+
"{prompt}",
|
|
55
|
+
"{model}"
|
|
56
|
+
],
|
|
57
|
+
"model": "ZCode current configured model",
|
|
58
|
+
"timeout_s": 1200
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"presets": {
|
|
62
|
+
"steady": {
|
|
63
|
+
"description": "默认稳定:Grok 做研究,Devin 做工程和审查,其他渠道备用",
|
|
64
|
+
"routes": {
|
|
65
|
+
"research": [
|
|
66
|
+
"grok",
|
|
67
|
+
"cursor",
|
|
68
|
+
"zcode",
|
|
69
|
+
"devin"
|
|
70
|
+
],
|
|
71
|
+
"engineering": [
|
|
72
|
+
"devin",
|
|
73
|
+
"zcode",
|
|
74
|
+
"cursor",
|
|
75
|
+
"grok"
|
|
76
|
+
],
|
|
77
|
+
"review": [
|
|
78
|
+
"devin",
|
|
79
|
+
"zcode",
|
|
80
|
+
"cursor",
|
|
81
|
+
"grok"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"retries": 3,
|
|
85
|
+
"retry_delays_s": [
|
|
86
|
+
30,
|
|
87
|
+
60,
|
|
88
|
+
120
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"cursor-rich": {
|
|
92
|
+
"description": "Cursor 额度充足:优先 Cursor,审查先用不同模型渠道",
|
|
93
|
+
"routes": {
|
|
94
|
+
"research": [
|
|
95
|
+
"cursor",
|
|
96
|
+
"grok",
|
|
97
|
+
"zcode",
|
|
98
|
+
"devin"
|
|
99
|
+
],
|
|
100
|
+
"engineering": [
|
|
101
|
+
"cursor",
|
|
102
|
+
"devin",
|
|
103
|
+
"zcode",
|
|
104
|
+
"grok"
|
|
105
|
+
],
|
|
106
|
+
"review": [
|
|
107
|
+
"zcode",
|
|
108
|
+
"devin",
|
|
109
|
+
"cursor",
|
|
110
|
+
"grok"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"retries": 3,
|
|
114
|
+
"retry_delays_s": [
|
|
115
|
+
30,
|
|
116
|
+
60,
|
|
117
|
+
120
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"zcode-rich": {
|
|
121
|
+
"description": "Z.ai 额度充足:ZCode 优先工程与审查,Grok 优先研究",
|
|
122
|
+
"routes": {
|
|
123
|
+
"research": [
|
|
124
|
+
"grok",
|
|
125
|
+
"zcode",
|
|
126
|
+
"cursor",
|
|
127
|
+
"devin"
|
|
128
|
+
],
|
|
129
|
+
"engineering": [
|
|
130
|
+
"zcode",
|
|
131
|
+
"devin",
|
|
132
|
+
"cursor",
|
|
133
|
+
"grok"
|
|
134
|
+
],
|
|
135
|
+
"review": [
|
|
136
|
+
"zcode",
|
|
137
|
+
"devin",
|
|
138
|
+
"cursor",
|
|
139
|
+
"grok"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
"retries": 3,
|
|
143
|
+
"retry_delays_s": [
|
|
144
|
+
30,
|
|
145
|
+
60,
|
|
146
|
+
120
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"core-only": {
|
|
150
|
+
"description": "保留 Cursor / ZCode 额度:只使用 Grok Build 和 Devin",
|
|
151
|
+
"routes": {
|
|
152
|
+
"research": [
|
|
153
|
+
"grok",
|
|
154
|
+
"devin"
|
|
155
|
+
],
|
|
156
|
+
"engineering": [
|
|
157
|
+
"devin",
|
|
158
|
+
"grok"
|
|
159
|
+
],
|
|
160
|
+
"review": [
|
|
161
|
+
"devin",
|
|
162
|
+
"grok"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"retries": 3,
|
|
166
|
+
"retry_delays_s": [
|
|
167
|
+
30,
|
|
168
|
+
60,
|
|
169
|
+
120
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""本机官方 CLI 入口;不读取或复制登录凭证。由外层 sandbox-exec 启动。"""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
provider, workdir, prompt_file, model = sys.argv[1:]
|
|
8
|
+
prompt = Path(prompt_file).read_text()
|
|
9
|
+
if provider == 'cursor':
|
|
10
|
+
binary = os.environ.get('WQ_CURSOR_BIN') or str(Path.home()/'.local/bin/cursor-agent')
|
|
11
|
+
argv = [binary, '--workspace', workdir, '--model', model, '--print', '--force',
|
|
12
|
+
'--trust', '--output-format', 'json', prompt]
|
|
13
|
+
elif provider == 'zcode':
|
|
14
|
+
os.environ['ZCODE_BUILTIN_PROVIDER_CONFIG_FILE'] = '/Applications/ZCode.app/Contents/Resources/config/provider/zcode-builtin.json'
|
|
15
|
+
binary = os.environ.get('WQ_NODE_BIN') or shutil.which('node')
|
|
16
|
+
if not binary:
|
|
17
|
+
raise SystemExit('node not found; set WQ_NODE_BIN')
|
|
18
|
+
argv = [binary, '/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs',
|
|
19
|
+
'--cwd', workdir, '--mode', 'yolo', '--json', '--prompt', prompt,
|
|
20
|
+
'--disallowed-tools', 'BrowserUse,WebSearch,WebFetch,Agent,Task']
|
|
21
|
+
else:
|
|
22
|
+
raise SystemExit('unknown provider')
|
|
23
|
+
os.execv(binary, argv)
|