@praveen-palanisamy/agent-browser-runtime 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +23 -0
  3. package/README.md +261 -0
  4. package/action.yml +185 -0
  5. package/dist/attribution.d.ts +22 -0
  6. package/dist/attribution.d.ts.map +1 -0
  7. package/dist/attribution.js +57 -0
  8. package/dist/attribution.js.map +1 -0
  9. package/dist/cli.d.ts +21 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +242 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/client/index.d.ts +9 -0
  14. package/dist/client/index.d.ts.map +1 -0
  15. package/dist/client/index.js +26 -0
  16. package/dist/client/index.js.map +1 -0
  17. package/dist/client/runner-client.d.ts +41 -0
  18. package/dist/client/runner-client.d.ts.map +1 -0
  19. package/dist/client/runner-client.js +98 -0
  20. package/dist/client/runner-client.js.map +1 -0
  21. package/dist/core/capture.d.ts +50 -0
  22. package/dist/core/capture.d.ts.map +1 -0
  23. package/dist/core/capture.js +129 -0
  24. package/dist/core/capture.js.map +1 -0
  25. package/dist/core/poster.d.ts +45 -0
  26. package/dist/core/poster.d.ts.map +1 -0
  27. package/dist/core/poster.js +132 -0
  28. package/dist/core/poster.js.map +1 -0
  29. package/dist/core/stores.d.ts +35 -0
  30. package/dist/core/stores.d.ts.map +1 -0
  31. package/dist/core/stores.js +32 -0
  32. package/dist/core/stores.js.map +1 -0
  33. package/dist/core/types.d.ts +138 -0
  34. package/dist/core/types.d.ts.map +1 -0
  35. package/dist/core/types.js +13 -0
  36. package/dist/core/types.js.map +1 -0
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +48 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/platform.d.ts +6 -0
  42. package/dist/platform.d.ts.map +1 -0
  43. package/dist/platform.js +3 -0
  44. package/dist/platform.js.map +1 -0
  45. package/dist/protocol.d.ts +84 -0
  46. package/dist/protocol.d.ts.map +1 -0
  47. package/dist/protocol.js +24 -0
  48. package/dist/protocol.js.map +1 -0
  49. package/dist/providers/kernel.d.ts +53 -0
  50. package/dist/providers/kernel.d.ts.map +1 -0
  51. package/dist/providers/kernel.js +82 -0
  52. package/dist/providers/kernel.js.map +1 -0
  53. package/dist/providers/playwright.d.ts +88 -0
  54. package/dist/providers/playwright.d.ts.map +1 -0
  55. package/dist/providers/playwright.js +114 -0
  56. package/dist/providers/playwright.js.map +1 -0
  57. package/dist/providers/steel.d.ts +48 -0
  58. package/dist/providers/steel.d.ts.map +1 -0
  59. package/dist/providers/steel.js +79 -0
  60. package/dist/providers/steel.js.map +1 -0
  61. package/dist/service/config.d.ts +33 -0
  62. package/dist/service/config.d.ts.map +1 -0
  63. package/dist/service/config.js +59 -0
  64. package/dist/service/config.js.map +1 -0
  65. package/dist/service/index.d.ts +30 -0
  66. package/dist/service/index.d.ts.map +1 -0
  67. package/dist/service/index.js +53 -0
  68. package/dist/service/index.js.map +1 -0
  69. package/dist/service/live-proxy.d.ts +27 -0
  70. package/dist/service/live-proxy.d.ts.map +1 -0
  71. package/dist/service/live-proxy.js +146 -0
  72. package/dist/service/live-proxy.js.map +1 -0
  73. package/dist/service/runner.d.ts +35 -0
  74. package/dist/service/runner.d.ts.map +1 -0
  75. package/dist/service/runner.js +139 -0
  76. package/dist/service/runner.js.map +1 -0
  77. package/dist/service/serialized-provider.d.ts +20 -0
  78. package/dist/service/serialized-provider.d.ts.map +1 -0
  79. package/dist/service/serialized-provider.js +68 -0
  80. package/dist/service/serialized-provider.js.map +1 -0
  81. package/dist/service/server.d.ts +17 -0
  82. package/dist/service/server.d.ts.map +1 -0
  83. package/dist/service/server.js +159 -0
  84. package/dist/service/server.js.map +1 -0
  85. package/docs/AGENTS.md +46 -0
  86. package/docs/ARCHITECTURE.md +73 -0
  87. package/docs/DEPLOY.md +61 -0
  88. package/docs/GITHUB_ACTION.md +92 -0
  89. package/examples/demo-strategy/index.js +53 -0
  90. package/examples/demo-strategy/session.json +5 -0
  91. package/package.json +96 -0
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal HTTP layer for the agent runner (no framework: `node:http` only).
4
+ *
5
+ * - Bearer-token auth on every /v1 route
6
+ * - JSON bodies, size-capped
7
+ * - /live/{captureId}/... proxies the provider live view (no token: the
8
+ * capture id is the credential, scoped to an active capture)
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.HttpError = void 0;
12
+ exports.isAuthorized = isAuthorized;
13
+ exports.createRunnerServer = createRunnerServer;
14
+ const node_crypto_1 = require("node:crypto");
15
+ const node_http_1 = require("node:http");
16
+ const attribution_1 = require("../attribution");
17
+ const protocol_1 = require("../protocol");
18
+ const live_proxy_1 = require("./live-proxy");
19
+ const MAX_BODY_BYTES = 8 * 1024 * 1024; // sessions with localStorage can be large
20
+ class HttpError extends Error {
21
+ status;
22
+ constructor(status, message) {
23
+ super(message);
24
+ this.status = status;
25
+ }
26
+ }
27
+ exports.HttpError = HttpError;
28
+ function json(res, status, body) {
29
+ res.writeHead(status, {
30
+ 'Content-Type': 'application/json',
31
+ Server: (0, attribution_1.userAgentString)(),
32
+ });
33
+ res.end(JSON.stringify(body));
34
+ }
35
+ async function readJson(req) {
36
+ const chunks = [];
37
+ let size = 0;
38
+ for await (const chunk of req) {
39
+ size += chunk.length;
40
+ if (size > MAX_BODY_BYTES) {
41
+ throw new HttpError(413, 'Request body too large');
42
+ }
43
+ chunks.push(chunk);
44
+ }
45
+ if (chunks.length === 0) {
46
+ return {};
47
+ }
48
+ try {
49
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
50
+ }
51
+ catch {
52
+ throw new HttpError(400, 'Invalid JSON body');
53
+ }
54
+ }
55
+ function isAuthorized(header, token) {
56
+ const presented = header?.startsWith('Bearer ') ? header.slice(7) : '';
57
+ const a = Buffer.from(presented);
58
+ const b = Buffer.from(token);
59
+ return a.length === b.length && (0, node_crypto_1.timingSafeEqual)(a, b);
60
+ }
61
+ function requireFields(body, fields) {
62
+ for (const f of fields) {
63
+ if (body[f] === undefined || body[f] === null || body[f] === '') {
64
+ throw new HttpError(400, `Missing required field: ${f}`);
65
+ }
66
+ }
67
+ }
68
+ function createRunnerServer(runner) {
69
+ const { config } = runner;
70
+ const liveDeps = {
71
+ resolve: (captureId) => runner.capture.liveViewUrlFor(captureId),
72
+ publicUrl: config.publicUrl,
73
+ livePrefix: protocol_1.RUNNER_ROUTES.live,
74
+ };
75
+ const server = (0, node_http_1.createServer)(async (req, res) => {
76
+ const url = new URL(req.url || '/', 'http://runner');
77
+ try {
78
+ if (req.method === 'GET' && url.pathname === protocol_1.RUNNER_ROUTES.health) {
79
+ json(res, 200, {
80
+ ok: true,
81
+ name: attribution_1.PROJECT.name,
82
+ version: (0, attribution_1.packageVersion)(),
83
+ repository: attribution_1.PROJECT.repository,
84
+ jobProvider: config.jobProvider,
85
+ captureProvider: config.captureProvider,
86
+ activeCaptures: runner.capture.size,
87
+ });
88
+ return;
89
+ }
90
+ if ((0, live_proxy_1.handleLiveHttp)(req, res, liveDeps)) {
91
+ return;
92
+ }
93
+ if (!url.pathname.startsWith('/v1/')) {
94
+ throw new HttpError(404, 'Not found');
95
+ }
96
+ if (!isAuthorized(req.headers.authorization, config.token)) {
97
+ throw new HttpError(401, 'Unauthorized');
98
+ }
99
+ if (req.method !== 'POST') {
100
+ throw new HttpError(405, 'Method not allowed');
101
+ }
102
+ const body = await readJson(req);
103
+ switch (url.pathname) {
104
+ case protocol_1.RUNNER_ROUTES.post:
105
+ requireFields(body, [
106
+ 'jobId',
107
+ 'workspaceId',
108
+ 'accountId',
109
+ 'platform',
110
+ 'content',
111
+ 'session',
112
+ ]);
113
+ json(res, 200, await runner.post(body));
114
+ return;
115
+ case protocol_1.RUNNER_ROUTES.probe:
116
+ requireFields(body, [
117
+ 'workspaceId',
118
+ 'accountId',
119
+ 'platform',
120
+ 'session',
121
+ ]);
122
+ json(res, 200, await runner.probe(body));
123
+ return;
124
+ case protocol_1.RUNNER_ROUTES.captureStart:
125
+ requireFields(body, ['platform']);
126
+ json(res, 200, await runner.captureStart(body));
127
+ return;
128
+ case protocol_1.RUNNER_ROUTES.captureFinish:
129
+ requireFields(body, ['captureId']);
130
+ json(res, 200, await runner.captureFinish(body));
131
+ return;
132
+ case protocol_1.RUNNER_ROUTES.captureCancel:
133
+ requireFields(body, ['captureId']);
134
+ json(res, 200, await runner.captureCancel(body));
135
+ return;
136
+ default:
137
+ throw new HttpError(404, 'Not found');
138
+ }
139
+ }
140
+ catch (e) {
141
+ if (e instanceof HttpError) {
142
+ json(res, e.status, { error: e.message });
143
+ return;
144
+ }
145
+ console.error('[agent-runner] request failed', url.pathname, e);
146
+ json(res, 500, {
147
+ error: e instanceof Error ? e.message : 'Internal error',
148
+ });
149
+ }
150
+ });
151
+ server.on('upgrade', (req, socket, head) => {
152
+ if (!(0, live_proxy_1.handleLiveUpgrade)(req, socket, head, liveDeps)) {
153
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
154
+ socket.destroy();
155
+ }
156
+ });
157
+ return server;
158
+ }
159
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/service/server.ts"],"names":[],"mappings":"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"}
package/docs/AGENTS.md ADDED
@@ -0,0 +1,46 @@
1
+ # Notes for coding agents and LLM tool builders
2
+
3
+ This file is written for AI agents (Cursor, Claude Code, Codex, custom MCP tools) that integrate or extend `agent-browser-runtime`.
4
+
5
+ ## What this package is, in one sentence
6
+
7
+ A runtime that lets software act inside a user's authenticated browser session, unattended, on infrastructure the operator controls, with explicit verification and re-auth signalling.
8
+
9
+ ## When to reach for it
10
+
11
+ - You need to perform actions on a website **as a specific user** and there is no API, the API is metered, or the API lacks the capability.
12
+ - The action must run **later** (cron, queue) without the user present.
13
+ - You need a **safe outcome model**: confirmed / uncertain / needs re-auth, so you never double-act.
14
+
15
+ ## When not to
16
+
17
+ - Scraping public pages (use a plain headless browser).
18
+ - Anything that violates the target site's terms or acts without the user's explicit instruction.
19
+
20
+ ## Integration recipe
21
+
22
+ 1. **Implement a `WebPostStrategy`** for the target site: `validate(content)`, `isAuthenticated(ctx)`, `post(ctx, content)`. Verify inside `post` (read back the UI or find the permalink); return `uncertain: true` if you submitted but could not confirm.
23
+ 2. **Run the service**: `startRunner({ strategies })` or `agent-browser-runtime serve --strategies ./strategies.js` with `AGENT_RUNNER_TOKEN`.
24
+ 3. **Capture once**: `captureStart` → embed `liveViewUrl` → user signs in → `captureFinish` → encrypt and store `state`.
25
+ 4. **Act unattended**: `post` / `probe` with the decrypted session; persist `refreshedSession`; honour `needsReauth` and `uncertain`.
26
+
27
+ ## Public API surface (stable)
28
+
29
+ - `@praveen-palanisamy/agent-browser-runtime` — contracts (`SessionStore`, `SessionProvider`, `WebPostStrategy`, `AgentSessionState`, `AgentPostResult`), `AgentPoster`, `SessionCaptureManager`, providers, `AgentRunnerClient`, protocol types.
30
+ - `…/client` — `AgentRunnerClient` + protocol only (no Playwright).
31
+ - `…/service` — `startRunner`, `AgentRunner`, `loadConfig`, `createRunnerServer`.
32
+ - CLI `agent-browser-runtime` — `serve`, `health`, `probe`, `post`; `--local` runs probe/post in-process (no service). Exit codes: `0` ok · `2` not authenticated / failed · `3` uncertain.
33
+ - GitHub Action `praveen-palanisamy/agent-browser-runtime@v0` — `command: probe|post`, outputs `ok`, `authenticated`, `needs-reauth`, `uncertain`, `result-file`, `session-file` (see `docs/GITHUB_ACTION.md`).
34
+ - `GET /healthz` returns `{ name, version, repository, jobProvider, captureProvider, activeCaptures }`; every JSON response carries a `Server: agent-browser-runtime/<version> (+repo)` header.
35
+
36
+ Wire protocol routes and request/response shapes live in `src/protocol.ts`; treat them as the contract when generating client code in other languages.
37
+
38
+ ## Extending
39
+
40
+ - **New provider**: implement `SessionProvider.acquire({ platform, state, interactive })` returning an `AgentSessionHandle` (`context`, `exportState`, `dispose`, optional `liveViewUrl`). Use `attachOverCdp` for any CDP-speaking backend. Add a unit test with a fake `fetch` (see `__tests__/kernel-provider.test.ts`).
41
+ - **New strategy**: keep it in your application; the runtime never hard-codes platforms.
42
+
43
+ ## Testing conventions
44
+
45
+ - `npm test` must stay browser-free and fast; anything that launches Chromium goes under `__tests__/browser/*.browser.test.ts`.
46
+ - Fake providers/handles are sufficient for orchestration tests (`__tests__/runner.test.ts`).
@@ -0,0 +1,73 @@
1
+ # Architecture
2
+
3
+ ## Layers
4
+
5
+ ```mermaid
6
+ flowchart TB
7
+ Client[client/ AgentRunnerClient<br/>no Playwright] -->|HTTP + bearer| Server[service/server.ts]
8
+ Server --> Runner[service/runner.ts AgentRunner]
9
+ Runner --> Poster[core/poster.ts AgentPoster]
10
+ Runner --> Capture[core/capture.ts SessionCaptureManager]
11
+ Poster --> Store[core/stores.ts InMemorySessionStore]
12
+ Poster --> Prov[SessionProvider]
13
+ Capture --> Prov
14
+ Prov --> Local[providers/playwright.ts<br/>LocalBrowserSessionProvider]
15
+ Prov --> Cdp[providers/playwright.ts<br/>CdpSessionProvider]
16
+ Prov --> Steel[providers/steel.ts]
17
+ Prov --> Kernel[providers/kernel.ts]
18
+ Server --> Live[service/live-proxy.ts]
19
+ ```
20
+
21
+ - **core** — pure orchestration over the three contracts. No HTTP, no env.
22
+ - **providers** — turn a stored session into a live `BrowserContext`. Remote providers create a browser via REST, connect over CDP, inject `storageState`, and dispose/release on close.
23
+ - **service** — env-driven runner: chooses providers per job type, registers the caller's strategies, exposes typed handlers and the HTTP server; proxies Steel's live view.
24
+ - **client / protocol** — the wire contract. Everything an orchestrator needs without pulling Playwright into its bundle.
25
+
26
+ ## Session lifecycle
27
+
28
+ ```mermaid
29
+ sequenceDiagram
30
+ participant U as User
31
+ participant A as Your app
32
+ participant R as Runner
33
+ participant B as Browser (Steel/Kernel/local)
34
+ A->>R: POST /v1/capture/start {platform}
35
+ R->>B: acquire(interactive)
36
+ R-->>A: captureId, liveViewUrl
37
+ A->>U: iframe liveViewUrl
38
+ U->>B: signs in
39
+ A->>R: POST /v1/capture/finish {captureId}
40
+ R->>B: isAuthenticated? exportState
41
+ R-->>A: AgentSessionState
42
+ A->>A: encrypt + persist
43
+ Note over A,R: later, unattended
44
+ A->>R: POST /v1/post {session, content}
45
+ R->>B: acquire(state) → strategy.isAuthenticated → strategy.post
46
+ R-->>A: AgentPostResult + refreshedSession
47
+ A->>A: persist refreshed session
48
+ ```
49
+
50
+ ## Verification model
51
+
52
+ A strategy is responsible for proving the action happened. `AgentPostResult` encodes three distinct outcomes so orchestrators can act safely:
53
+
54
+ | Result | Meaning | Recommended orchestrator behaviour |
55
+ |--------|---------|-------------------------------------|
56
+ | `ok: true` + `verification` | Action confirmed (toast/permalink or read-back) | Mark done, persist `refreshedSession` |
57
+ | `ok: false`, `uncertain: true` | Submit happened but nothing confirmed it | Flag for manual review; **do not** retry or fall back automatically |
58
+ | `ok: false`, `needsReauth: true` | Session invalid | Mark account needs re-auth, notify user, optional fallback |
59
+ | `ok: false`, `failureStage` ∈ precheck/compose/media | Aborted **before** submitting | Safe to retry or fall back |
60
+ | `ok: false`, `failureStage: submit/verify` (not uncertain) | Failed with evidence nothing was posted | Safe to retry/fallback; inspect screenshot |
61
+
62
+ ## Pacing
63
+
64
+ `AgentPoster` applies a small randomized delay before acting (`pacing: true`) to look human-scale. Per-account rate limits (minimum spacing, hourly caps) belong in the orchestrator where account state lives; the runner is stateless.
65
+
66
+ ## Provider selection
67
+
68
+ `RunnerConfig.jobProvider` and `captureProvider` are independent. Typical production layout:
69
+
70
+ - jobs → `local` (parallel, fresh Chromium per job inside the runner container)
71
+ - capture → `steel` (live view) or `kernel` (headful browser with live view URL)
72
+
73
+ `SerializedProvider` wraps single-session backends (Steel OSS) with a mutex so concurrent requests queue instead of failing.
package/docs/DEPLOY.md ADDED
@@ -0,0 +1,61 @@
1
+ # Deploying and integrating ABR
2
+
3
+ ## Environment
4
+
5
+ | Variable | Required | Notes |
6
+ |----------|----------|-------|
7
+ | `AGENT_RUNNER_TOKEN` | yes | ≥ 16 chars; shared with orchestrators |
8
+ | `AGENT_RUNNER_PUBLIC_URL` | for capture | Absolute URL users reach the runner at (live view links) |
9
+ | `PORT` | no | default 8080 |
10
+ | `AGENT_RUNNER_JOB_PROVIDER` | no | `local` (default) · `steel` · `kernel` |
11
+ | `AGENT_RUNNER_CAPTURE_PROVIDER` | no | `steel` when `STEEL_API_URL` is set, else `local` |
12
+ | `STEEL_API_URL` / `STEEL_API_KEY` | for steel | e.g. `http://steel:3000`; key only for Steel Cloud |
13
+ | `KERNEL_API_KEY` / `KERNEL_API_URL` | for kernel | default `https://api.onkernel.com` |
14
+ | `AGENT_RUNNER_CAPTURE_TTL_MS` | no | default 20 min |
15
+ | `AGENT_RUNNER_PACING` | no | `false` disables human-pace jitter (tests) |
16
+
17
+ ## Local (Docker Compose)
18
+
19
+ ```bash
20
+ AGENT_RUNNER_TOKEN=$(openssl rand -base64 32) STRATEGIES_DIR=$PWD/examples/demo-strategy docker compose up --build
21
+ curl -H "authorization: Bearer $AGENT_RUNNER_TOKEN" localhost:8080/healthz
22
+ ```
23
+
24
+ Steel is exposed on `:3000` (API + live view) and `:9223` (CDP). Only the runtime should reach them in production.
25
+
26
+ ## Container image
27
+
28
+ `ghcr.io/praveen-palanisamy/agent-browser-runtime:<version>` bundles Node, Playwright's Chromium and the runtime CLI. Extend it with your strategies:
29
+
30
+ ```dockerfile
31
+ FROM ghcr.io/praveen-palanisamy/agent-browser-runtime:latest
32
+ COPY dist/strategies.js /app/strategies.js
33
+ CMD ["serve", "--strategies", "/app/strategies.js"]
34
+ ```
35
+
36
+ The image pins `playwright-core` to the base image's Playwright version (`ARG PLAYWRIGHT_VERSION`), verified in CI by `scripts/check-playwright-pin.mjs`, so no browser download happens at runtime.
37
+
38
+ ## Cloud Run (runtime + Steel sidecar)
39
+
40
+ - One service, two containers: the runtime (ingress) and `ghcr.io/steel-dev/steel-browser-api` on `localhost:3000`.
41
+ - Set `STEEL_API_URL=http://127.0.0.1:3000`, `AGENT_RUNNER_CAPTURE_PROVIDER=steel`, `AGENT_RUNNER_JOB_PROVIDER=local`.
42
+ - Interactive captures are stateful (a browser stays open while the user signs in), so pin `minScale = maxScale = 1` or add sticky routing. Headless jobs are parallel inside the instance.
43
+ - Ingress must be public for the live view; every API route is still token-protected. Store `AGENT_RUNNER_TOKEN` in Secret Manager.
44
+ - Give the Steel container ≥ 2 GiB memory and mount an in-memory volume at `/app/.cache`; nothing durable lives there.
45
+
46
+ The same shape works on ECS/Fargate task definitions or a Nomad group.
47
+
48
+ ## Scaling beyond one instance
49
+
50
+ - Move captures to **Kernel** (`AGENT_RUNNER_CAPTURE_PROVIDER=kernel`): each capture gets its own microVM with a live view URL, so instances become interchangeable.
51
+ - Or run several runner instances and route `/v1/capture/finish|cancel` and `/live/{captureId}` by capture id (hash or header) at the load balancer.
52
+
53
+ ## Ways to consume ABR
54
+
55
+ | Channel | Best for | Pin |
56
+ |---------|----------|-----|
57
+ | [npm `@praveen-palanisamy/agent-browser-runtime`](https://www.npmjs.com/package/@praveen-palanisamy/agent-browser-runtime) | embedding the library or client in your app | `^0.1` |
58
+ | [GHCR `ghcr.io/praveen-palanisamy/agent-browser-runtime`](https://github.com/praveen-palanisamy/agent-browser-runtime/pkgs/container/agent-browser-runtime) | running the runner service (Chromium included) | `:0.1`, `:latest` |
59
+ | [GitHub Action](GITHUB_ACTION.md) | probes and jobs from CI without a service | `@v0` |
60
+
61
+ Releases are cut automatically from `main`; every semver tag publishes all three channels with provenance. Follow the [Releases page](https://github.com/praveen-palanisamy/agent-browser-runtime/releases) or watch the [changelog](../CHANGELOG.md).
@@ -0,0 +1,92 @@
1
+ # GitHub Action
2
+
3
+ ABR ships as a composite action so you can run authenticated browser jobs from any workflow — no runner service to deploy. It installs the runtime and Chromium on the job runner and executes the CLI in-process (`--local`).
4
+
5
+ ```yaml
6
+ - uses: praveen-palanisamy/agent-browser-runtime@v0
7
+ with:
8
+ command: probe # or: post
9
+ platform: expense-portal
10
+ strategies: ./automation/strategies.js
11
+ session: ${{ secrets.EXPENSE_PORTAL_SESSION }}
12
+ ```
13
+
14
+ ## Typical workflows
15
+
16
+ ### Nightly session health check
17
+
18
+ Tell users *before* a scheduled job fails that they need to sign in again.
19
+
20
+ ```yaml
21
+ name: session-health
22
+ on:
23
+ schedule: [{ cron: "0 6 * * *" }]
24
+ jobs:
25
+ probe:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v5
29
+ - id: abr
30
+ uses: praveen-palanisamy/agent-browser-runtime@v0
31
+ with:
32
+ command: probe
33
+ platform: expense-portal
34
+ strategies: ./automation/strategies.js
35
+ session: ${{ secrets.EXPENSE_PORTAL_SESSION }}
36
+ fail-on-reauth: "false"
37
+ - if: steps.abr.outputs.needs-reauth == 'true'
38
+ run: ./notify.sh "Expense portal session expired — please sign in again"
39
+ ```
40
+
41
+ ### Run a job with the stored session
42
+
43
+ ```yaml
44
+ - id: abr
45
+ uses: praveen-palanisamy/agent-browser-runtime@v0
46
+ with:
47
+ command: post
48
+ platform: expense-portal
49
+ strategies: ./automation/strategies.js
50
+ session: ${{ secrets.EXPENSE_PORTAL_SESSION }}
51
+ text: "March travel — client onsite"
52
+ media: https://files.example/receipt-1.jpg,https://files.example/receipt-2.jpg
53
+ - name: Persist rotated cookies
54
+ if: always()
55
+ run: ./vault put expense-portal-session "${{ steps.abr.outputs.session-file }}"
56
+ - name: Never retry blindly
57
+ if: steps.abr.outputs.uncertain == 'true'
58
+ run: ./open-review-ticket.sh "${{ steps.abr.outputs.result-file }}"
59
+ ```
60
+
61
+ ## Inputs
62
+
63
+ | Input | Default | Notes |
64
+ |-------|---------|-------|
65
+ | `command` | `probe` | `probe` or `post` |
66
+ | `platform` | — | must match a strategy's `platform` |
67
+ | `session` / `session-file` | — | `AgentSessionState` JSON; pass a secret or a file path |
68
+ | `strategies` | bundled demo | module exporting `WebPostStrategy[]` |
69
+ | `text`, `media`, `link`, `content-file` | — | job content (`post`) |
70
+ | `workspace`, `account` | `github-actions` / `default` | job identity |
71
+ | `session-out` | `.abr/session.refreshed.json` | refreshed session written here |
72
+ | `result-out` | `.abr/result.json` | JSON result written here |
73
+ | `fail-on-reauth` | `true` | fail the step when re-auth is needed |
74
+ | `pacing` | `true` | human-pace jitter before acting |
75
+ | `node-version` | `24` | |
76
+
77
+ ## Outputs
78
+
79
+ `ok`, `authenticated`, `needs-reauth`, `uncertain`, `result-file`, `session-file`.
80
+
81
+ Exit codes of the underlying CLI: `0` success · `2` not authenticated / failed · `3` uncertain (ran, unverified).
82
+
83
+ ## Security notes
84
+
85
+ - Store sessions as **encrypted secrets**; the action writes the secret to a temp file and deletes it after the run.
86
+ - Refreshed sessions are written to `session-out` in the workspace — persist them back to your vault and do not upload them as public artifacts.
87
+ - The action runs on GitHub's runner IPs. Sites that fingerprint location may still ask for re-auth; for stable egress, run the [HTTP runner service](DEPLOY.md) instead.
88
+ - Use ABR only with the user's explicit authorization and within each site's terms of service.
89
+
90
+ ## Versioning
91
+
92
+ `@v0` tracks the latest green commit on `main` (moved automatically); pin `@v0.1.0` for immutable builds.
@@ -0,0 +1,53 @@
1
+ // Minimal WebPostStrategy module for `agent-browser-runtime serve --strategies`.
2
+ // It drives example.com so it works anywhere without credentials; real
3
+ // strategies implement the same three methods against a platform's UI.
4
+ //
5
+ // AGENT_RUNNER_TOKEN=... npx agent-browser-runtime serve --strategies ./examples/demo-strategy/index.js
6
+
7
+ /** @type {import('@praveen-palanisamy/agent-browser-runtime').WebPostStrategy} */
8
+ const demoStrategy = {
9
+ platform: 'demo',
10
+ loginUrl: 'https://example.com/',
11
+
12
+ validate(content) {
13
+ if (!content.text?.trim()) return 'Text is required';
14
+ if (content.text.length > 280) return 'Text exceeds 280 characters';
15
+ return null;
16
+ },
17
+
18
+ async isAuthenticated(context) {
19
+ // A real strategy checks for a logged-in marker (avatar, compose button…).
20
+ const page = await context.newPage();
21
+ try {
22
+ await page.goto('https://example.com/', {
23
+ waitUntil: 'domcontentloaded',
24
+ });
25
+ return (await page.title()).length > 0;
26
+ } finally {
27
+ await page.close();
28
+ }
29
+ },
30
+
31
+ async post(context, _content) {
32
+ const page = await context.newPage();
33
+ try {
34
+ await page.goto('https://example.com/', {
35
+ waitUntil: 'domcontentloaded',
36
+ });
37
+ // Verification is the strategy's job: read back what the UI shows and
38
+ // only report ok when it matches `content`.
39
+ const heading = await page.textContent('h1');
40
+ return heading
41
+ ? {
42
+ ok: true,
43
+ platformPostId: `demo-${Date.now()}`,
44
+ verification: 'toast',
45
+ }
46
+ : { ok: false, failureStage: 'verify', error: 'no heading rendered' };
47
+ } finally {
48
+ await page.close();
49
+ }
50
+ },
51
+ };
52
+
53
+ module.exports = [demoStrategy];
@@ -0,0 +1,5 @@
1
+ {
2
+ "platform": "demo",
3
+ "storageState": { "cookies": [], "origins": [] },
4
+ "capturedAt": "2026-01-01T00:00:00.000Z"
5
+ }
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@praveen-palanisamy/agent-browser-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Stateful, secure browser-session execution for agents: pluggable browser providers (local Chromium, CDP, Steel, Kernel), post/probe/capture orchestration, an HTTP runner service and a Playwright-free client",
5
+ "keywords": [
6
+ "agents",
7
+ "ai-agents",
8
+ "authenticated-sessions",
9
+ "browser-agent",
10
+ "browser-automation",
11
+ "cdp",
12
+ "github-action",
13
+ "headless-chrome",
14
+ "kernel",
15
+ "kernel-browsers",
16
+ "playwright",
17
+ "rpa",
18
+ "self-hosted",
19
+ "session",
20
+ "steel",
21
+ "steel-browser",
22
+ "storage-state",
23
+ "web-automation"
24
+ ],
25
+ "license": "MIT",
26
+ "author": "Praveen Palanisamy",
27
+ "homepage": "https://praveen-palanisamy.github.io/agent-browser-runtime/",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/praveen-palanisamy/agent-browser-runtime.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/praveen-palanisamy/agent-browser-runtime/issues"
34
+ },
35
+ "main": "dist/index.js",
36
+ "types": "dist/index.d.ts",
37
+ "bin": {
38
+ "agent-browser-runtime": "./dist/cli.js"
39
+ },
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "./client": {
46
+ "types": "./dist/client/index.d.ts",
47
+ "default": "./dist/client/index.js"
48
+ },
49
+ "./service": {
50
+ "types": "./dist/service/index.d.ts",
51
+ "default": "./dist/service/index.js"
52
+ }
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "docs",
57
+ "examples",
58
+ "README.md",
59
+ "CHANGELOG.md",
60
+ "LICENSE",
61
+ "action.yml"
62
+ ],
63
+ "engines": {
64
+ "node": ">=22"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "scripts": {
70
+ "build": "tsc -p tsconfig.json",
71
+ "typecheck": "tsc -p tsconfig.json --noEmit",
72
+ "lint": "biome check .",
73
+ "format": "biome check --write .",
74
+ "test": "vitest run",
75
+ "test:browser": "ABR_TEST_SUITE=browser vitest run",
76
+ "browsers:install": "npx playwright-core install chromium",
77
+ "ci:check": "npm run lint && npm run typecheck && npm test && node scripts/check-playwright-pin.mjs && node scripts/check-action-manifest.mjs",
78
+ "pack:check": "npm pack --dry-run",
79
+ "release:prepare": "node scripts/release.mjs",
80
+ "prepublishOnly": "npm run build",
81
+ "site:build": "node scripts/site-build.mjs",
82
+ "site:serve": "node scripts/site-build.mjs && npx -y serve site/dist",
83
+ "site:demos": "node scripts/site-demos.mjs"
84
+ },
85
+ "dependencies": {
86
+ "playwright-core": "^1.57.0"
87
+ },
88
+ "devDependencies": {
89
+ "@biomejs/biome": "^2.5.12",
90
+ "@types/node": "^26.4.1",
91
+ "marked": "^18.0.11",
92
+ "typescript": "^7.0.2",
93
+ "vitest": "^5.0.0"
94
+ },
95
+ "funding": "https://github.com/sponsors/praveen-palanisamy"
96
+ }