typeclaw 0.1.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/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { definePlugin } from '@/plugin'
|
|
4
|
+
import { bindWithForward } from '@/portbroker'
|
|
5
|
+
|
|
6
|
+
import { AGENT_BROWSER_DASHBOARD_PROXY_PORT, startDashboardProxy, type DashboardProxy } from './dashboard-proxy'
|
|
7
|
+
import { installShim, KNOWN_BIN_PATHS, type InstallShimResult } from './shim-install'
|
|
8
|
+
|
|
9
|
+
type SafeResult = InstallShimResult | { kind: 'error'; binPath: string; error: unknown }
|
|
10
|
+
|
|
11
|
+
// Documented in skills/agent-browser/SKILL.md so the agent can discover which
|
|
12
|
+
// port the proxy actually bound to (4848 + a 10-port fallback range). Moving
|
|
13
|
+
// or renaming this path requires updating the skill in lockstep.
|
|
14
|
+
const PROXY_PORT_HINT_PATH = '/tmp/typeclaw-agent-browser-proxy-port'
|
|
15
|
+
const PORT_CANDIDATE_RANGE = 10
|
|
16
|
+
const BROKER_HANDSHAKE_DELAY_MS = 1_000
|
|
17
|
+
const FORWARD_RESULT_TIMEOUT_MS = 10_000
|
|
18
|
+
|
|
19
|
+
let activeProxy: DashboardProxy | null = null
|
|
20
|
+
let bindingInFlight: Promise<void> | null = null
|
|
21
|
+
|
|
22
|
+
export default definePlugin({
|
|
23
|
+
plugin: async (ctx) => {
|
|
24
|
+
for (const binPath of Object.values(KNOWN_BIN_PATHS)) {
|
|
25
|
+
logInstallResult(ctx.logger, safeInstallShim(binPath))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Kick off the proxy bind in the background and let the plugin factory
|
|
29
|
+
// return immediately. Two reasons:
|
|
30
|
+
// 1. The container-side broker is created AFTER pluginsLoaded.markBooted()
|
|
31
|
+
// runs (see src/run/index.ts). If we awaited bindWithForward here, we
|
|
32
|
+
// would block the boot sequence past 20s of timeouts before the broker
|
|
33
|
+
// even existed to send forward-result events.
|
|
34
|
+
// 2. The dashboard isn't typically used at boot — the user runs
|
|
35
|
+
// `agent-browser dashboard start` later. The proxy has plenty of time
|
|
36
|
+
// to settle before its first request.
|
|
37
|
+
if (activeProxy === null && bindingInFlight === null) {
|
|
38
|
+
bindingInFlight = bindProxyAfterBrokerSettles(ctx.logger).finally(() => {
|
|
39
|
+
bindingInFlight = null
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
skillsDirs: [join(import.meta.dir, 'skills')],
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export function __resetProxyForTesting(): void {
|
|
50
|
+
activeProxy?.stop()
|
|
51
|
+
activeProxy = null
|
|
52
|
+
bindingInFlight = null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function __waitForProxyBindForTesting(): Promise<void> {
|
|
56
|
+
return bindingInFlight ?? Promise.resolve()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function bindProxyAfterBrokerSettles(logger: {
|
|
60
|
+
info: (msg: string) => void
|
|
61
|
+
warn: (msg: string) => void
|
|
62
|
+
}): Promise<void> {
|
|
63
|
+
// Give the run-loop time to construct the container broker and let it
|
|
64
|
+
// complete its WS handshake with hostd. Without this the first candidate
|
|
65
|
+
// bind fires before the broker is ready, the bus never delivers a result,
|
|
66
|
+
// and we waste the full timeout × candidate-count budget tearing down
|
|
67
|
+
// every port in the range. The exact delay isn't load-bearing — anything
|
|
68
|
+
// longer than the broker's connect+hello round-trip works.
|
|
69
|
+
if (defaultBrokerEnabled()) {
|
|
70
|
+
await Bun.sleep(BROKER_HANDSHAKE_DELAY_MS)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const config = readPortConfig()
|
|
74
|
+
const candidates = buildCandidatePorts(config.listenPort)
|
|
75
|
+
const upstreamOverride = config.upstreamPort
|
|
76
|
+
|
|
77
|
+
const result = await bindWithForward<DashboardProxy>({
|
|
78
|
+
candidates,
|
|
79
|
+
timeoutMs: FORWARD_RESULT_TIMEOUT_MS,
|
|
80
|
+
factory: (port) => {
|
|
81
|
+
try {
|
|
82
|
+
const proxy = startDashboardProxy({ listenPort: port, upstreamPort: upstreamOverride })
|
|
83
|
+
return Promise.resolve({ resource: proxy, close: () => proxy.stop() })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.warn(`bind ${port} failed: ${String(error)}`)
|
|
86
|
+
return Promise.resolve(null)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
onLog: (msg) => logger.info(`[bind-with-forward] ${msg}`),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (result === null) {
|
|
93
|
+
logger.warn(
|
|
94
|
+
`could not allocate a host-forwardable dashboard proxy port from ${candidates[0]}-${candidates[candidates.length - 1]}; ` +
|
|
95
|
+
`remote dashboard access will not work until another container releases its port`,
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
activeProxy = result.resource
|
|
101
|
+
recordProxyPort(result.port, logger)
|
|
102
|
+
logger.info(
|
|
103
|
+
`dashboard proxy listening on port ${result.port}` +
|
|
104
|
+
(result.hostPort !== null ? ` (forwarded to host:${result.hostPort})` : ''),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function defaultBrokerEnabled(): boolean {
|
|
109
|
+
const token = process.env['TYPECLAW_HOSTD_BROKER_TOKEN']
|
|
110
|
+
return token !== undefined && token.length > 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildCandidatePorts(start: number): number[] {
|
|
114
|
+
const out: number[] = []
|
|
115
|
+
for (let i = 0; i < PORT_CANDIDATE_RANGE; i += 1) out.push(start + i)
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function recordProxyPort(port: number, logger: { warn: (msg: string) => void }): void {
|
|
120
|
+
try {
|
|
121
|
+
Bun.write(PROXY_PORT_HINT_PATH, String(port))
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Hint is informational (lets a future `typeclaw status` or a human shell
|
|
124
|
+
// session report which port to open). Failure is non-fatal.
|
|
125
|
+
logger.warn(`failed to write ${PROXY_PORT_HINT_PATH}: ${String(error)}`)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type PortConfig = { listenPort: number; upstreamPort: number | undefined }
|
|
130
|
+
|
|
131
|
+
function readPortConfig(): PortConfig {
|
|
132
|
+
const overrideUpstream = process.env['TYPECLAW_DASHBOARD_UPSTREAM_PORT']
|
|
133
|
+
return {
|
|
134
|
+
listenPort: numberFromEnv('TYPECLAW_DASHBOARD_PROXY_PORT', AGENT_BROWSER_DASHBOARD_PROXY_PORT),
|
|
135
|
+
upstreamPort:
|
|
136
|
+
overrideUpstream === undefined || overrideUpstream === '' ? undefined : numberOrUndefined(overrideUpstream),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function numberOrUndefined(raw: string): number | undefined {
|
|
141
|
+
const parsed = Number(raw)
|
|
142
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) return undefined
|
|
143
|
+
return parsed
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function numberFromEnv(name: string, fallback: number): number {
|
|
147
|
+
const raw = process.env[name]
|
|
148
|
+
if (raw === undefined || raw === '') return fallback
|
|
149
|
+
const parsed = Number(raw)
|
|
150
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) return fallback
|
|
151
|
+
return parsed
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function safeInstallShim(binPath: string): SafeResult {
|
|
155
|
+
try {
|
|
156
|
+
return installShim({ binPath })
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return { kind: 'error', binPath, error }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function logInstallResult(
|
|
163
|
+
logger: { info: (msg: string) => void; warn: (msg: string) => void },
|
|
164
|
+
result: SafeResult,
|
|
165
|
+
): void {
|
|
166
|
+
if (result.kind === 'installed') {
|
|
167
|
+
logger.info(`installed agent-browser shim at ${result.binPath} (real bin: ${result.realBin})`)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (result.kind === 'already-installed') {
|
|
171
|
+
logger.info(`agent-browser shim already installed at ${result.binPath}`)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (result.kind === 'no-upstream') {
|
|
175
|
+
logger.info(`no agent-browser binary at ${result.binPath}; skipping`)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
logger.warn(`failed to install agent-browser shim at ${result.binPath}: ${String(result.error)}`)
|
|
179
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstatSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
readlinkSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
statSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { dirname, join, resolve as resolvePath } from 'node:path'
|
|
12
|
+
|
|
13
|
+
import { REAL_BIN_ENV } from './shim'
|
|
14
|
+
|
|
15
|
+
const DEFAULT_GLOBAL_BIN_PATH = '/usr/local/bin/agent-browser'
|
|
16
|
+
const DEFAULT_LOCAL_BIN_PATH = '/agent/node_modules/.bin/agent-browser'
|
|
17
|
+
const STASH_ROOT = '/usr/local/lib/typeclaw-agent-browser'
|
|
18
|
+
const SHIM_MARKER = '# typeclaw-agent-browser-shim'
|
|
19
|
+
|
|
20
|
+
export type InstallShimOptions = {
|
|
21
|
+
binPath?: string
|
|
22
|
+
stashDir?: string
|
|
23
|
+
shimEntry?: string
|
|
24
|
+
fs?: ShimFs
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ShimFs = {
|
|
28
|
+
lstat: (path: string) => { isSymbolicLink: () => boolean } | null
|
|
29
|
+
// statExists follows symlinks: a broken symlink (link entry exists, target
|
|
30
|
+
// does not) returns false. This is the discriminator we need to skip
|
|
31
|
+
// installation when the host bind-mount surfaces dangling node_modules/.bin
|
|
32
|
+
// entries inside the container — see installShim's upstream guard.
|
|
33
|
+
statExists: (path: string) => boolean
|
|
34
|
+
readlink: (path: string) => string
|
|
35
|
+
readFile: (path: string) => string
|
|
36
|
+
rename: (from: string, to: string) => void
|
|
37
|
+
symlink: (target: string, path: string) => void
|
|
38
|
+
writeFile: (path: string, data: string, mode: number) => void
|
|
39
|
+
unlink: (path: string) => void
|
|
40
|
+
mkdirp: (path: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type InstallShimResult =
|
|
44
|
+
| { kind: 'installed'; realBin: string; binPath: string; stashTarget: string }
|
|
45
|
+
| { kind: 'already-installed'; binPath: string }
|
|
46
|
+
| { kind: 'no-upstream'; binPath: string }
|
|
47
|
+
|
|
48
|
+
export function installShim(opts: InstallShimOptions = {}): InstallShimResult {
|
|
49
|
+
const binPath = opts.binPath ?? DEFAULT_GLOBAL_BIN_PATH
|
|
50
|
+
const shimEntry = opts.shimEntry ?? defaultShimEntry()
|
|
51
|
+
const fs = opts.fs ?? defaultFs()
|
|
52
|
+
const stashDir = opts.stashDir ?? defaultStashDir(binPath)
|
|
53
|
+
const stashTarget = join(stashDir, 'agent-browser-real')
|
|
54
|
+
|
|
55
|
+
const stat = fs.lstat(binPath)
|
|
56
|
+
if (stat === null) return { kind: 'no-upstream', binPath }
|
|
57
|
+
|
|
58
|
+
if (isAlreadyShim(binPath, fs)) {
|
|
59
|
+
if (fs.statExists(stashTarget)) return { kind: 'already-installed', binPath }
|
|
60
|
+
// Wrapper survived a container restart but the image-owned stash did not
|
|
61
|
+
// (STASH_ROOT lives outside the bind-mount). The wrapper now points at a
|
|
62
|
+
// non-existent stashTarget, so executing it would ENOENT. Drop it and
|
|
63
|
+
// report no-upstream — there is nothing valid here to preserve, and the
|
|
64
|
+
// global-path shim (if it exists) stands on its own.
|
|
65
|
+
fs.unlink(binPath)
|
|
66
|
+
return { kind: 'no-upstream', binPath }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bind-mounted node_modules/.bin entries can be dangling symlinks inside
|
|
70
|
+
// the container (host ran bun install; the container image did not). lstat
|
|
71
|
+
// alone passes for those. Follow the link with statExists before mutating
|
|
72
|
+
// anything — otherwise we'd stash a broken symlink and write a wrapper
|
|
73
|
+
// pointing at a target that never resolves.
|
|
74
|
+
if (!fs.statExists(binPath)) return { kind: 'no-upstream', binPath }
|
|
75
|
+
|
|
76
|
+
const realBin = resolveCurrentTarget(binPath, stat, fs)
|
|
77
|
+
fs.mkdirp(stashDir)
|
|
78
|
+
if (stat.isSymbolicLink()) {
|
|
79
|
+
fs.unlink(binPath)
|
|
80
|
+
if (fs.lstat(stashTarget) !== null) fs.unlink(stashTarget)
|
|
81
|
+
fs.symlink(realBin, stashTarget)
|
|
82
|
+
} else {
|
|
83
|
+
if (fs.lstat(stashTarget) !== null) fs.unlink(stashTarget)
|
|
84
|
+
fs.rename(binPath, stashTarget)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.writeFile(binPath, renderWrapper(shimEntry, stashTarget), 0o755)
|
|
88
|
+
return { kind: 'installed', realBin, binPath, stashTarget }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const KNOWN_BIN_PATHS = {
|
|
92
|
+
global: DEFAULT_GLOBAL_BIN_PATH,
|
|
93
|
+
local: DEFAULT_LOCAL_BIN_PATH,
|
|
94
|
+
} as const
|
|
95
|
+
|
|
96
|
+
function defaultStashDir(binPath: string): string {
|
|
97
|
+
// Per-binPath subdirectory under the image-owned stash root. Lives outside
|
|
98
|
+
// every bind-mounted agent folder so a host-side `bun install` cannot
|
|
99
|
+
// touch it; the wrapper at the bind-mounted location can be clobbered by
|
|
100
|
+
// host-side installs but the stash and image-level real binary stay safe.
|
|
101
|
+
const slug = binPath.replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
102
|
+
return join(STASH_ROOT, slug)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isAlreadyShim(binPath: string, fs: ShimFs): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return fs.readFile(binPath).includes(SHIM_MARKER)
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveCurrentTarget(binPath: string, stat: { isSymbolicLink: () => boolean }, fs: ShimFs): string {
|
|
114
|
+
if (!stat.isSymbolicLink()) return binPath
|
|
115
|
+
const target = fs.readlink(binPath)
|
|
116
|
+
return resolvePath(dirname(binPath), target)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderWrapper(shimEntry: string, stashTarget: string): string {
|
|
120
|
+
return `#!/bin/sh
|
|
121
|
+
${SHIM_MARKER}
|
|
122
|
+
export ${REAL_BIN_ENV}="\${${REAL_BIN_ENV}:-${stashTarget}}"
|
|
123
|
+
exec bun run ${shimEntry} "$@"
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function defaultShimEntry(): string {
|
|
128
|
+
return resolvePath(import.meta.dir, 'shim.ts')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function defaultFs(): ShimFs {
|
|
132
|
+
return {
|
|
133
|
+
lstat: (path) => {
|
|
134
|
+
try {
|
|
135
|
+
return lstatSync(path)
|
|
136
|
+
} catch {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
statExists: (path) => {
|
|
141
|
+
try {
|
|
142
|
+
statSync(path)
|
|
143
|
+
return true
|
|
144
|
+
} catch {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
readlink: readlinkSync,
|
|
149
|
+
readFile: (path) => readFileSync(path, 'utf-8'),
|
|
150
|
+
rename: renameSync,
|
|
151
|
+
symlink: symlinkSync,
|
|
152
|
+
writeFile: (path, data, mode) => writeFileSync(path, data, { mode }),
|
|
153
|
+
unlink: unlinkSync,
|
|
154
|
+
mkdirp: (path) => {
|
|
155
|
+
Bun.spawnSync(['mkdir', '-p', path])
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// PATH-shadow shim installed at the global bin path that `bun install -g
|
|
2
|
+
// agent-browser` previously occupied. Replaces the plugin's old prompt-nudge +
|
|
3
|
+
// bash-regex `tool.before` block, which was leaky (missed shell variations,
|
|
4
|
+
// bypassed by `typeclaw shell`, by spawned subprocesses, by the user typing
|
|
5
|
+
// it directly). Now ANY in-container `agent-browser` caller routes through
|
|
6
|
+
// here — the dashboard subcommand transparently gets its --port rewritten
|
|
7
|
+
// onto the agent-process-owned proxy's upstream port, every other subcommand
|
|
8
|
+
// passes through unchanged. The proxy itself lives in the long-lived agent
|
|
9
|
+
// process (see src/bundled-plugins/agent-browser/index.ts); the shim does NOT own its
|
|
10
|
+
// lifecycle, because `agent-browser dashboard start` daemonizes upstream and
|
|
11
|
+
// returns immediately — a shim-owned proxy would die the moment start exits.
|
|
12
|
+
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
|
|
15
|
+
import { writePortHint } from './dashboard-discovery'
|
|
16
|
+
import { AGENT_BROWSER_DASHBOARD_UPSTREAM_PORT } from './dashboard-proxy'
|
|
17
|
+
|
|
18
|
+
export const REAL_BIN_ENV = 'TYPECLAW_AGENT_BROWSER_REAL_BIN'
|
|
19
|
+
|
|
20
|
+
export type DashboardIntent = 'start' | 'stop' | 'other'
|
|
21
|
+
|
|
22
|
+
export function classifyDashboardCommand(argv: readonly string[]): DashboardIntent {
|
|
23
|
+
// Find the first non-flag token. `agent-browser` takes no pre-subcommand
|
|
24
|
+
// global flags today; the loop is defensive against future ones.
|
|
25
|
+
let dashboardIdx = -1
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const arg = argv[i]!
|
|
28
|
+
if (arg.startsWith('-')) continue
|
|
29
|
+
if (arg !== 'dashboard') return 'other'
|
|
30
|
+
dashboardIdx = i
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
if (dashboardIdx === -1) return 'other'
|
|
34
|
+
|
|
35
|
+
// Look for the next non-flag token after `dashboard`. Upstream treats a
|
|
36
|
+
// missing subcommand as `start`, so we do too. `--port <n>` and `-p <n>`
|
|
37
|
+
// consume two argv entries; the value is not a subcommand and must not be
|
|
38
|
+
// classified as one.
|
|
39
|
+
for (let i = dashboardIdx + 1; i < argv.length; i += 1) {
|
|
40
|
+
const arg = argv[i]!
|
|
41
|
+
if (arg === '--port' || arg === '-p') {
|
|
42
|
+
i += 1
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (arg.startsWith('-')) continue
|
|
46
|
+
if (arg === 'stop') return 'stop'
|
|
47
|
+
if (arg === 'start') return 'start'
|
|
48
|
+
return 'other'
|
|
49
|
+
}
|
|
50
|
+
return 'start'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function rewriteDashboardArgs(argv: readonly string[], upstreamPort: number): string[] {
|
|
54
|
+
// Force --port to upstreamPort regardless of what the caller passed. The
|
|
55
|
+
// proxy on AGENT_BROWSER_DASHBOARD_PROXY_PORT (4848) is the only externally
|
|
56
|
+
// visible surface; honoring a user --port would let a caller bypass the
|
|
57
|
+
// proxy by listening directly on the externally forwarded port. Insert
|
|
58
|
+
// `start` explicitly when the caller relied on the implicit-start behavior
|
|
59
|
+
// so the appended `--port` lands on a subcommand upstream accepts.
|
|
60
|
+
const stripped: string[] = []
|
|
61
|
+
let i = 0
|
|
62
|
+
while (i < argv.length) {
|
|
63
|
+
const arg = argv[i]!
|
|
64
|
+
if (arg === '--port' || arg === '-p') {
|
|
65
|
+
i += 2
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (arg.startsWith('--port=')) {
|
|
69
|
+
i += 1
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
stripped.push(arg)
|
|
73
|
+
i += 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dashboardIdx = stripped.findIndex((a) => !a.startsWith('-'))
|
|
77
|
+
const hasSubcommand = stripped.slice(dashboardIdx + 1).some((a) => !a.startsWith('-'))
|
|
78
|
+
const out = hasSubcommand
|
|
79
|
+
? [...stripped]
|
|
80
|
+
: [...stripped.slice(0, dashboardIdx + 1), 'start', ...stripped.slice(dashboardIdx + 1)]
|
|
81
|
+
out.push('--port', String(upstreamPort))
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resolveRealAgentBrowserBin(): string {
|
|
86
|
+
// Set by the installer when it moves the upstream symlink aside. Honored
|
|
87
|
+
// first so unit tests can point at a stub without touching the filesystem.
|
|
88
|
+
const fromEnv = process.env[REAL_BIN_ENV]
|
|
89
|
+
if (fromEnv && fromEnv.length > 0) return fromEnv
|
|
90
|
+
|
|
91
|
+
// Fallback: `bun install -g agent-browser` ships per-platform native bins
|
|
92
|
+
// under this stable path inside the bun image. The installer should have
|
|
93
|
+
// stashed a copy/symlink, but if the shim runs before the plugin's
|
|
94
|
+
// installer ever did (e.g. first agent boot), we can still find the real
|
|
95
|
+
// bin and the next plugin boot will install the shim properly.
|
|
96
|
+
const arch = process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'x64' : null
|
|
97
|
+
const platform = process.platform === 'linux' ? 'linux' : process.platform === 'darwin' ? 'darwin' : null
|
|
98
|
+
if (arch !== null && platform !== null) {
|
|
99
|
+
const native = `/root/.bun/install/global/node_modules/agent-browser/bin/agent-browser-${platform}-${arch}`
|
|
100
|
+
if (existsSync(native)) return native
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${REAL_BIN_ENV} is not set and no fallback agent-browser binary was found. ` +
|
|
105
|
+
`The shim cannot resolve the real upstream binary; refusing to exec to avoid an infinite loop.`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ShimOptions = {
|
|
110
|
+
argv?: readonly string[]
|
|
111
|
+
realBin?: string
|
|
112
|
+
upstreamPort?: number
|
|
113
|
+
spawn?: (cmd: string[]) => { exited: Promise<number> }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runShim(opts: ShimOptions = {}): Promise<number> {
|
|
117
|
+
const argv = opts.argv ?? process.argv.slice(2)
|
|
118
|
+
const realBin = opts.realBin ?? resolveRealAgentBrowserBin()
|
|
119
|
+
const upstreamPort = opts.upstreamPort ?? AGENT_BROWSER_DASHBOARD_UPSTREAM_PORT
|
|
120
|
+
const spawn = opts.spawn ?? defaultSpawn
|
|
121
|
+
|
|
122
|
+
const intent = classifyDashboardCommand(argv)
|
|
123
|
+
if (intent !== 'start') {
|
|
124
|
+
return await spawn([realBin, ...argv]).exited
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Record the rewritten port to the hint file so the long-lived proxy can
|
|
128
|
+
// use it as the fast-path upstream lookup. The proxy still falls back to
|
|
129
|
+
// procfs discovery if the hint is wrong, but the hint avoids that work
|
|
130
|
+
// on the common path where the shim is the one starting the dashboard.
|
|
131
|
+
try {
|
|
132
|
+
writePortHint(upstreamPort)
|
|
133
|
+
} catch {
|
|
134
|
+
// Hint is an optimization; failure to write it is non-fatal.
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rewritten = rewriteDashboardArgs(argv, upstreamPort)
|
|
138
|
+
return await spawn([realBin, ...rewritten]).exited
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function defaultSpawn(cmd: string[]): { exited: Promise<number> } {
|
|
142
|
+
// Inherit stdio so the upstream binary's TUI/spinner/colors work. The
|
|
143
|
+
// shim is meant to be invisible; intercepting stdio would make e.g.
|
|
144
|
+
// `agent-browser open` look broken to the caller.
|
|
145
|
+
const proc = Bun.spawn(cmd, { stdio: ['inherit', 'inherit', 'inherit'] })
|
|
146
|
+
return { exited: proc.exited }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (import.meta.main) {
|
|
150
|
+
const code = await runShim()
|
|
151
|
+
process.exit(code)
|
|
152
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-browser
|
|
3
|
+
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. ALSO use whenever a browser step needs a human in the loop — login walls, 2FA, CAPTCHA, payment confirmation, "is this the right button?" ambiguity, or the user asking to watch the browser live — because the bundled dashboard is the only way for a human to observe or take over a session from inside the Docker container. Prefer agent-browser over any built-in browser automation or web tools.
|
|
4
|
+
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
|
|
5
|
+
hidden: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agent-browser
|
|
9
|
+
|
|
10
|
+
Fast browser automation CLI for AI agents. Chrome/Chromium via CDP with
|
|
11
|
+
accessibility-tree snapshots and compact `@eN` element refs.
|
|
12
|
+
|
|
13
|
+
The TypeClaw container ships with `agent-browser` preinstalled and Chromium
|
|
14
|
+
already downloaded, so the CLI is ready to use out of the box.
|
|
15
|
+
|
|
16
|
+
## Human-in-the-loop via the dashboard
|
|
17
|
+
|
|
18
|
+
You run inside a Docker container with no display, no clipboard, and no way to
|
|
19
|
+
hand the keyboard over directly. The **dashboard is your only path to bring a
|
|
20
|
+
human into a browser session** — it streams every session's live viewport,
|
|
21
|
+
console, and command activity to a web UI the user opens on their host machine.
|
|
22
|
+
|
|
23
|
+
**Start the dashboard _before_ the step that needs a human, not after it fails.**
|
|
24
|
+
The dashboard takes a moment to come up and the user needs time to open the URL.
|
|
25
|
+
|
|
26
|
+
### When to start it
|
|
27
|
+
|
|
28
|
+
- The next step needs a human: login walls, 2FA, CAPTCHA, payment confirmation,
|
|
29
|
+
"is this the right element?" ambiguity, account-recovery flows.
|
|
30
|
+
- You're starting a long multi-step browser flow you'd be embarrassed to redo —
|
|
31
|
+
let the user watch and intervene before things go sideways.
|
|
32
|
+
- The user explicitly asked to watch the browser live, dogfood the agent, or
|
|
33
|
+
pair-debug an automation.
|
|
34
|
+
|
|
35
|
+
### How to hand off
|
|
36
|
+
|
|
37
|
+
1. Run `agent-browser dashboard start`. (Sessions auto-stream to it; no flags
|
|
38
|
+
needed.)
|
|
39
|
+
2. Read `/tmp/typeclaw-agent-browser-proxy-port` to learn the host-visible
|
|
40
|
+
port. TypeClaw picks `4848` by default and falls back through `4849`–`4857`
|
|
41
|
+
if another container is already on `4848`. If the file is missing, the proxy
|
|
42
|
+
hasn't finished binding yet — wait a second and retry, or fall back to `4848`.
|
|
43
|
+
3. Tell the user: **"Open `http://localhost:<port>` in your browser."** Over
|
|
44
|
+
Tailscale or LAN, the same port works on the host's external address:
|
|
45
|
+
`http://<host>:<port>`.
|
|
46
|
+
4. Wait for the user to confirm they're ready before proceeding.
|
|
47
|
+
5. When the user is done, they hand control back implicitly — just resume your
|
|
48
|
+
normal `agent-browser` commands. Session state is shared with the dashboard.
|
|
49
|
+
|
|
50
|
+
The compatibility proxy on `:4848` (or the fallback port) rewrites the
|
|
51
|
+
dashboard's hardcoded loopback URLs so the externally visible URL works over
|
|
52
|
+
Tailscale and other remote networks. No special flag, tool, or config required.
|
|
53
|
+
**Always share the proxy port URL — never `localhost:<raw-session-port>`** —
|
|
54
|
+
those raw ports are inside the container and unreachable from the host.
|
|
55
|
+
|
|
56
|
+
### When NOT to use the dashboard
|
|
57
|
+
|
|
58
|
+
The dashboard is for **live observation and handoff**, not file delivery. If
|
|
59
|
+
you just want to show the user a single page or a captured state:
|
|
60
|
+
|
|
61
|
+
- **A static image?** Use `agent-browser screenshot`; the PNG lands in
|
|
62
|
+
`workspace/` and the user can open it directly.
|
|
63
|
+
- **A page's text/structure?** Capture an accessibility-tree snapshot and paste
|
|
64
|
+
the relevant section into your reply.
|
|
65
|
+
|
|
66
|
+
Reserve the dashboard for cases that genuinely need live interaction or
|
|
67
|
+
watching a multi-step flow unfold.
|
|
68
|
+
|
|
69
|
+
### Headless only
|
|
70
|
+
|
|
71
|
+
Never pass `--headed` to any `agent-browser` command — the container has no X
|
|
72
|
+
server or `$DISPLAY`, and a headed launch fails with `Missing X server or
|
|
73
|
+
$DISPLAY / The platform failed to initialize.` The dashboard is the substitute
|
|
74
|
+
for a headed browser. Use the default headless mode for everything, including
|
|
75
|
+
dogfooding and Electron flows.
|
|
76
|
+
|
|
77
|
+
## Start here
|
|
78
|
+
|
|
79
|
+
This file is a discovery stub, not the usage guide. Before running any
|
|
80
|
+
`agent-browser` command, load the actual workflow content from the CLI:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
agent-browser skills get core # start here — workflows, common patterns, troubleshooting
|
|
84
|
+
agent-browser skills get core --full # include full command reference and templates
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The CLI serves skill content that always matches the installed version,
|
|
88
|
+
so instructions never go stale. The content in this stub cannot change
|
|
89
|
+
between releases, which is why it just points at `skills get core`.
|
|
90
|
+
|
|
91
|
+
## Specialized skills
|
|
92
|
+
|
|
93
|
+
Load a specialized skill when the task falls outside browser web pages:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
agent-browser skills get electron # Electron desktop apps (VS Code, Slack, Discord, Figma, ...)
|
|
97
|
+
agent-browser skills get slack # Slack workspace automation
|
|
98
|
+
agent-browser skills get dogfood # Exploratory testing / QA / bug hunts
|
|
99
|
+
agent-browser skills get vercel-sandbox # agent-browser inside Vercel Sandbox microVMs
|
|
100
|
+
agent-browser skills get agentcore # AWS Bedrock AgentCore cloud browsers
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run `agent-browser skills list` to see everything available on the
|
|
104
|
+
installed version.
|
|
105
|
+
|
|
106
|
+
## Why agent-browser
|
|
107
|
+
|
|
108
|
+
- Fast native Rust CLI, not a Node.js wrapper
|
|
109
|
+
- Works with any AI agent (Cursor, Claude Code, Codex, Continue, Windsurf, etc.)
|
|
110
|
+
- Chrome/Chromium via CDP with no Playwright or Puppeteer dependency
|
|
111
|
+
- Accessibility-tree snapshots with element refs for reliable interaction
|
|
112
|
+
- Sessions, authentication vault, state persistence, video recording
|
|
113
|
+
- Specialized skills for Electron apps, Slack, exploratory testing, cloud providers
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { definePlugin } from '@/plugin'
|
|
2
|
+
|
|
3
|
+
import { checkNonWorkspaceWriteGuard, checkSkillAuthoringGuard, checkUncommittedChangesAdvice } from './policy'
|
|
4
|
+
|
|
5
|
+
export default definePlugin({
|
|
6
|
+
plugin: async () => ({
|
|
7
|
+
hooks: {
|
|
8
|
+
'tool.before': async (event, ctx) => {
|
|
9
|
+
const skillResult = await checkSkillAuthoringGuard({
|
|
10
|
+
tool: event.tool,
|
|
11
|
+
args: event.args,
|
|
12
|
+
agentDir: ctx.agentDir,
|
|
13
|
+
})
|
|
14
|
+
if (skillResult) return skillResult
|
|
15
|
+
return checkNonWorkspaceWriteGuard({ tool: event.tool, args: event.args, agentDir: ctx.agentDir })
|
|
16
|
+
},
|
|
17
|
+
'tool.after': async (event, ctx) => {
|
|
18
|
+
await checkUncommittedChangesAdvice({
|
|
19
|
+
tool: event.tool,
|
|
20
|
+
agentDir: ctx.agentDir,
|
|
21
|
+
result: event.result,
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
})
|