evot-agent 0.4.1 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evot-agent",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Local browser agent for evot — browses sites with Claude or Codex and appends results as cells.",
6
6
  "license": "MIT",
@@ -30,7 +30,26 @@ export async function runClaudeJob(job) {
30
30
  preset: "claude_code",
31
31
  append: SYSTEM_APPEND,
32
32
  },
33
- disallowedTools: ["Bash", "Write", "Edit", "NotebookEdit"],
33
+ // 내장 툴을 전부 비운다. allowedTools는 "확인 없이 자동 승인할 목록"이지 제한이
34
+ // 아니라서(SDK: "To restrict which tools are available, use the `tools` option
35
+ // instead"), 예전처럼 allowedTools에만 mcp__playwright를 넣으면 Read·Grep·WebFetch·
36
+ // WebSearch가 그대로 살아 있고 bypassPermissions 탓에 확인도 없이 실행됐다.
37
+ // tools는 "내장" 툴 집합만 지정하며(SDK가 --tools ""로 전달), MCP 서버는 mcpServers로
38
+ // 따로 붙으므로 Playwright는 그대로 남는다.
39
+ tools: [],
40
+ // 이름 기준 이중 차단 — tools:[]가 이미 막지만, 상위 설정 변화에 대비해 남긴다.
41
+ disallowedTools: [
42
+ "Bash",
43
+ "Write",
44
+ "Edit",
45
+ "NotebookEdit",
46
+ "Read",
47
+ "Glob",
48
+ "Grep",
49
+ "WebFetch",
50
+ "WebSearch",
51
+ "Agent",
52
+ ],
34
53
  mcpServers: {
35
54
  playwright: {
36
55
  type: "stdio",
@@ -38,6 +57,7 @@ export async function runClaudeJob(job) {
38
57
  args: mcp.args,
39
58
  },
40
59
  },
60
+ // MCP 툴은 프롬프트 없이 실행되도록 자동 승인 유지.
41
61
  allowedTools: ["mcp__playwright"],
42
62
  },
43
63
  })) {
@@ -63,13 +63,18 @@ export const SYSTEM_APPEND = [
63
63
  */
64
64
  export function buildPlaywrightMcp(headless) {
65
65
  const playwrightArgs = [
66
- "@playwright/mcp",
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 (/codex is not signed in|codex login/i.test(msg)) {
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 백엔드 미로그인/인증 실패 — Codex 대칭 보강.
147
- if (/not logged in|invalid api key|\/login|unauthorized|authentication|credential/i.test(msg)) {
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
  "",
@@ -27,9 +27,10 @@ const BLOCKED = new Set([
27
27
  ]);
28
28
 
29
29
  const BLOCK_MESSAGE =
30
- "This tool is blocked: the evot agent is read-only and cannot perform write actions " +
31
- "(form fill/submit, uploads, native <select>, drag/drop, dialogs, arbitrary JS). " +
32
- "Read and summarize what is visible instead, or report that the task needs a write action.";
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
@@ -55,11 +55,13 @@ async function runJob(job, localBackend) {
55
55
  start_url: job.start_url ?? undefined,
56
56
  headless: true,
57
57
  },
58
- { backend },
58
+ // delegated: 구독 위임 잡(내 지시가 아니라 구독자 지시로 내 머신에서 도는 잡).
59
+ // 현재 서버는 위임 잡을 만들지 않아 항상 false — 재도입 대비 안전망.
60
+ { backend, delegated: job.delegated === true },
59
61
  ));
60
62
  } catch (err) {
61
63
  console.error(`Job ${job.id} failed: ${err.message}`);
62
- text = describeJobFailure(err);
64
+ text = describeJobFailure(err, backend);
63
65
  status = "error";
64
66
  }
65
67
  try {