evot-agent 0.4.1 → 0.5.1
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/README.md +1 -1
- package/bin/evot-agent.js +1 -1
- package/package.json +1 -1
- package/src/backends/claude.js +20 -1
- package/src/backends/common.js +6 -1
- package/src/browse.js +32 -7
- package/src/readonly-proxy.js +8 -4
- package/src/sync.js +8 -4
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npx evot-agent connect <code> # 예: npx evot-agent connect ABCD2345
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npm install -g evot-agent
|
|
40
|
-
evot-agent start # 기본
|
|
40
|
+
evot-agent start # 기본 1분마다 폴링
|
|
41
41
|
evot-agent install # macOS 전용: 로그인 시 자동 시작 + 백그라운드 상주
|
|
42
42
|
evot-agent uninstall # 상주 등록 해제
|
|
43
43
|
```
|
package/bin/evot-agent.js
CHANGED
|
@@ -15,7 +15,7 @@ Normal use is "start": leave it running, then ask a Vot (in chat or a routine) t
|
|
|
15
15
|
"use my computer's browser to …". evot queues the task; this daemon picks it up and
|
|
16
16
|
appends the result as a cell in the target thread.
|
|
17
17
|
|
|
18
|
-
start polling: default every
|
|
18
|
+
start polling: default every 60s (1 min). To change it use --interval,
|
|
19
19
|
or set EVOT_AGENT_POLL_SECONDS. Minimum 30s.
|
|
20
20
|
|
|
21
21
|
Backend (which model drives the browser): defaults to Claude (uses your Claude Code
|
package/package.json
CHANGED
package/src/backends/claude.js
CHANGED
|
@@ -30,7 +30,25 @@ export async function runClaudeJob(job) {
|
|
|
30
30
|
preset: "claude_code",
|
|
31
31
|
append: SYSTEM_APPEND,
|
|
32
32
|
},
|
|
33
|
-
|
|
33
|
+
// tools 옵션은 쓰지 않는다. SDK가 tools:[]를 `--tools ""`로 넘기는데, 이건 내장 툴만이
|
|
34
|
+
// 아니라 MCP 툴까지 전부 비운다. 그러면 Playwright가 사라진 채로 모델이 브라우저 없이
|
|
35
|
+
// 결과를 날조하고, subtype:"success"로 끝나 아래 isError 판정에도 안 걸린다
|
|
36
|
+
// (2026-08-21 실측: init tools=[], 실제 tool_use 0회, 툴 호출을 본문 텍스트로 위조).
|
|
37
|
+
// tools에 mcp__playwright 툴 이름을 명시해도 동일하게 비므로 이 옵션으론 우회가 안 된다.
|
|
38
|
+
// → 내장 툴 차단은 disallowedTools(이름 기준) 한 겹으로만 건다. allowedTools는 "확인 없이
|
|
39
|
+
// 자동 승인할 목록"이지 제한이 아니므로 차단 용도로 믿으면 안 된다.
|
|
40
|
+
disallowedTools: [
|
|
41
|
+
"Bash",
|
|
42
|
+
"Write",
|
|
43
|
+
"Edit",
|
|
44
|
+
"NotebookEdit",
|
|
45
|
+
"Read",
|
|
46
|
+
"Glob",
|
|
47
|
+
"Grep",
|
|
48
|
+
"WebFetch",
|
|
49
|
+
"WebSearch",
|
|
50
|
+
"Agent",
|
|
51
|
+
],
|
|
34
52
|
mcpServers: {
|
|
35
53
|
playwright: {
|
|
36
54
|
type: "stdio",
|
|
@@ -38,6 +56,7 @@ export async function runClaudeJob(job) {
|
|
|
38
56
|
args: mcp.args,
|
|
39
57
|
},
|
|
40
58
|
},
|
|
59
|
+
// MCP 툴은 프롬프트 없이 실행되도록 자동 승인 유지.
|
|
41
60
|
allowedTools: ["mcp__playwright"],
|
|
42
61
|
},
|
|
43
62
|
})) {
|
package/src/backends/common.js
CHANGED
|
@@ -63,13 +63,18 @@ export const SYSTEM_APPEND = [
|
|
|
63
63
|
*/
|
|
64
64
|
export function buildPlaywrightMcp(headless) {
|
|
65
65
|
const playwrightArgs = [
|
|
66
|
-
|
|
66
|
+
// 버전 고정: readonly-proxy의 차단 목록은 이 버전의 툴 셋에 맞춰져 있다. 전역 설치(~/.evot-agent)
|
|
67
|
+
// 에서 npx는 로컬 node_modules가 없어 미고정 시 레지스트리 최신을 받으므로, 새 버전이 추가한
|
|
68
|
+
// 쓰기 툴이 차단을 우회할 수 있다. package.json의 @playwright/mcp 핀과 일치시킨다.
|
|
69
|
+
"@playwright/mcp@0.0.77",
|
|
67
70
|
"--browser",
|
|
68
71
|
"chrome",
|
|
69
72
|
"--user-data-dir",
|
|
70
73
|
PROFILE_DIR,
|
|
71
74
|
];
|
|
72
75
|
if (headless) playwrightArgs.push("--headless");
|
|
76
|
+
// NOTE: posix(macOS/Linux) 기준. Windows에서는 프록시가 "npx"를 그대로 spawn하면
|
|
77
|
+
// ENOENT/.cmd 스폰 정책에 걸릴 수 있어 별도 검증이 필요하다(현재 미검증 — 백로그).
|
|
73
78
|
return {
|
|
74
79
|
command: process.execPath,
|
|
75
80
|
args: [READONLY_PROXY_PATH, "npx", ...playwrightArgs],
|
package/src/browse.js
CHANGED
|
@@ -10,12 +10,22 @@ const BACKENDS = {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 실행 백엔드를 고른다. 우선순위: 명시 인자 > EVOT_AGENT_BACKEND > 기본 "claude".
|
|
13
|
+
*
|
|
14
|
+
* delegated(구독 위임 잡)면 codex를 거부한다. codex는 read-only 샌드박스라 쓰기·네트워크는
|
|
15
|
+
* 막아도 내장 shell의 읽기는 허용이라, 남의 지시로 내 머신에서 도는 잡에는 허용할 수 없다
|
|
16
|
+
* (claude는 tools:[]로 shell·Read가 아예 없다). 현재 서버가 위임 잡을 만들지 않아 항상
|
|
17
|
+
* false지만, 위임이 되살아나도 Codex 경로가 열리지 않도록 남겨둔 안전망이다.
|
|
13
18
|
*/
|
|
14
|
-
export function resolveBackend(explicit) {
|
|
19
|
+
export function resolveBackend(explicit, opts = {}) {
|
|
15
20
|
const name = (explicit || process.env.EVOT_AGENT_BACKEND || "claude").toLowerCase();
|
|
16
21
|
if (!BACKENDS[name]) {
|
|
17
22
|
throw new Error(`Unknown backend "${name}". Use "claude" or "codex".`);
|
|
18
23
|
}
|
|
24
|
+
if (opts.delegated && name === "codex") {
|
|
25
|
+
const err = new Error("Codex runtime is not allowed for delegated tasks.");
|
|
26
|
+
err.delegatedBackendBlocked = true;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
19
29
|
return name;
|
|
20
30
|
}
|
|
21
31
|
|
|
@@ -66,7 +76,7 @@ export function parseSegments(raw) {
|
|
|
66
76
|
* 백엔드는 최종 텍스트만 책임지고, LOGIN_REQUIRED 마커·빈결과 판정·파싱은 여기서 공통 처리한다.
|
|
67
77
|
*/
|
|
68
78
|
export async function runBrowseJob(job, opts = {}) {
|
|
69
|
-
const backend = resolveBackend(opts.backend);
|
|
79
|
+
const backend = resolveBackend(opts.backend, { delegated: opts.delegated });
|
|
70
80
|
const finalText = await BACKENDS[backend](job);
|
|
71
81
|
|
|
72
82
|
// 세션만료 마커: 모델이 로그인 벽에 막혀 정상 종료해도 실패로 다룬다.
|
|
@@ -102,8 +112,21 @@ function isSafeDomain(d) {
|
|
|
102
112
|
* 잡 실패를 셀/텔레그램에 남길 사용자용 텍스트로 조립한다.
|
|
103
113
|
* loginDomain(세션만료 마커)이 있으면 재로그인 절차를 안내한다.
|
|
104
114
|
*/
|
|
105
|
-
export function describeJobFailure(err) {
|
|
115
|
+
export function describeJobFailure(err, backend) {
|
|
106
116
|
const msg = err?.message ?? "";
|
|
117
|
+
// 활성 백엔드(claude/codex). 미로그인 안내를 실제 실행 백엔드에 한해서만 내보내,
|
|
118
|
+
// 방문한 웹페이지의 "unauthorized/login" 문구를 CLI 미로그인으로 오인하는 것을 막는다.
|
|
119
|
+
const runtime = (backend || "claude").toLowerCase();
|
|
120
|
+
|
|
121
|
+
// 위임 잡인데 런타임이 Codex — 백엔드 선택 단계에서 거부된 경우.
|
|
122
|
+
if (err?.delegatedBackendBlocked) {
|
|
123
|
+
return [
|
|
124
|
+
"This task came from a Vot subscription, and the connected computer is set to the Codex runtime.",
|
|
125
|
+
"Subscription tasks only run on the Claude Code runtime for safety.",
|
|
126
|
+
"",
|
|
127
|
+
"To enable them, switch the runtime to Claude Code in Settings → Computer Use on that computer.",
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
107
130
|
|
|
108
131
|
if (err?.loginDomain && isSafeDomain(err.loginDomain)) {
|
|
109
132
|
return [
|
|
@@ -133,8 +156,8 @@ export function describeJobFailure(err) {
|
|
|
133
156
|
].join("\n");
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
// Codex 백엔드 미로그인 — 유저 컴퓨터에 Codex가 없거나
|
|
137
|
-
if (
|
|
159
|
+
// Codex 백엔드 미로그인 — codex 실행일 때만. 유저 컴퓨터에 Codex가 없거나 미로그인.
|
|
160
|
+
if (runtime === "codex" && /not signed in|codex login/i.test(msg)) {
|
|
138
161
|
return [
|
|
139
162
|
"Codex is not signed in on the connected computer, so this task could not run.",
|
|
140
163
|
"",
|
|
@@ -143,8 +166,10 @@ export function describeJobFailure(err) {
|
|
|
143
166
|
].join("\n");
|
|
144
167
|
}
|
|
145
168
|
|
|
146
|
-
// Claude Code 백엔드 미로그인/인증 실패 —
|
|
147
|
-
|
|
169
|
+
// Claude Code 백엔드 미로그인/인증 실패 — claude 실행일 때만. 정규식은 SDK가 내는
|
|
170
|
+
// 인증 에러(로그인/API 키)에 한정한다. 웹페이지의 401 "unauthorized/authentication"을
|
|
171
|
+
// CLI 미로그인으로 오인하지 않도록 그런 범용 단어는 매칭에서 제외한다.
|
|
172
|
+
if (runtime === "claude" && /not logged in|invalid api key|please run.*\/login|\bno api key\b/i.test(msg)) {
|
|
148
173
|
return [
|
|
149
174
|
"Claude Code is not logged in on the connected computer, so this task could not run.",
|
|
150
175
|
"",
|
package/src/readonly-proxy.js
CHANGED
|
@@ -27,9 +27,10 @@ const BLOCKED = new Set([
|
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
29
|
const BLOCK_MESSAGE =
|
|
30
|
-
"This tool is blocked
|
|
31
|
-
"
|
|
32
|
-
"Read and summarize what is visible instead, or report that the task needs
|
|
30
|
+
"This tool is blocked. The evot agent runs in a restricted mode: bulk form fill, " +
|
|
31
|
+
"file uploads, native <select>, drag/drop, dialogs, and arbitrary JS execution are " +
|
|
32
|
+
"disabled. Read and summarize what is visible instead, or report that the task needs " +
|
|
33
|
+
"one of these blocked actions.";
|
|
33
34
|
|
|
34
35
|
const childArgs = process.argv.slice(2);
|
|
35
36
|
if (childArgs.length === 0) {
|
|
@@ -42,11 +43,14 @@ const child = spawn(childArgs[0], childArgs.slice(1), {
|
|
|
42
43
|
stdio: ["pipe", "pipe", "inherit"],
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
46
|
+
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
46
47
|
child.on("error", (err) => {
|
|
47
48
|
process.stderr.write(`readonly-proxy: failed to start child: ${err.message}\n`);
|
|
48
49
|
process.exit(1);
|
|
49
50
|
});
|
|
51
|
+
// child가 먼저 종료된 뒤의 write는 EPIPE를 던진다 — 프록시 크래시 대신 조용히 무시
|
|
52
|
+
// (파이프가 닫히면 클라이언트가 stdout 종료를 감지해 잡 에러로 표면화한다).
|
|
53
|
+
child.stdin.on("error", () => {});
|
|
50
54
|
|
|
51
55
|
function writeToClient(obj) {
|
|
52
56
|
process.stdout.write(JSON.stringify(obj) + "\n");
|
package/src/sync.js
CHANGED
|
@@ -2,8 +2,10 @@ import { SUPABASE_URL, loadToken } from "./config.js";
|
|
|
2
2
|
import { describeJobFailure, runBrowseJob } from "./browse.js";
|
|
3
3
|
import { reportResult } from "./report.js";
|
|
4
4
|
|
|
5
|
-
// 폴링 주기: --interval <sec> > EVOT_AGENT_POLL_SECONDS > 기본
|
|
6
|
-
|
|
5
|
+
// 폴링 주기: --interval <sec> > EVOT_AGENT_POLL_SECONDS > 기본 60초(1분). 최소 30초.
|
|
6
|
+
// 잡 생성(루틴 발동·대화)부터 실행까지의 지연을 폴링 대기가 지배해서 600초에서 낮췄다.
|
|
7
|
+
// 더 줄여도 앞단의 cron(1분 주기)과 vot-react 추론 시간에 묻혀 체감 이득이 없다.
|
|
8
|
+
const DEFAULT_INTERVAL_SEC = 60;
|
|
7
9
|
const MIN_INTERVAL_SEC = 30;
|
|
8
10
|
|
|
9
11
|
function resolveIntervalSec(cliSec) {
|
|
@@ -55,11 +57,13 @@ async function runJob(job, localBackend) {
|
|
|
55
57
|
start_url: job.start_url ?? undefined,
|
|
56
58
|
headless: true,
|
|
57
59
|
},
|
|
58
|
-
|
|
60
|
+
// delegated: 구독 위임 잡(내 지시가 아니라 구독자 지시로 내 머신에서 도는 잡).
|
|
61
|
+
// 현재 서버는 위임 잡을 만들지 않아 항상 false — 재도입 대비 안전망.
|
|
62
|
+
{ backend, delegated: job.delegated === true },
|
|
59
63
|
));
|
|
60
64
|
} catch (err) {
|
|
61
65
|
console.error(`Job ${job.id} failed: ${err.message}`);
|
|
62
|
-
text = describeJobFailure(err);
|
|
66
|
+
text = describeJobFailure(err, backend);
|
|
63
67
|
status = "error";
|
|
64
68
|
}
|
|
65
69
|
try {
|