@dynamicagents/plugins 0.17.0 → 0.18.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/README.md CHANGED
@@ -38,7 +38,7 @@ export class Coder extends StepAgent<Env> {
38
38
 
39
39
  override getPlugins() {
40
40
  return [
41
- browser({ binding: this.env.BROWSER }),
41
+ browser({ binding: this.env.BROWSER, loader: this.env.LOADER }),
42
42
  computer({ shell: "bash" }),
43
43
  repo(repoConfig(this.env))
44
44
  ];
@@ -66,7 +66,7 @@ plugin to the tools an install names.
66
66
 
67
67
  | Subpath | What it adds | Needs |
68
68
  | ---------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
69
- | [`/browser`](src/browser/) | Read web pages via Browser Rendering Quick Actions | `BROWSER` (paid plan) |
69
+ | [`/browser`](src/browser/) | Drive a persistent browser by writing code, over Browser Run | `BROWSER` (paid plan), `LOADER` |
70
70
  | [`/claude-code`](src/claude-code/) | Sub-agents whose model is a Claude Code session in a sandbox | one or more `claude setup-token` credentials |
71
71
  | [`/computer`](src/computer/) | An agent's tools over a sandbox — `exec`, `grep`, `edit` — and the workspace Think's file tools use | a `/sandbox` object |
72
72
  | [`/repo-github`](src/repo-github/) | Clone, commit, push a branch, open a pull request on GitHub — over any container | `GITHUB_TOKEN` |
@@ -1,28 +1,25 @@
1
1
  import type { AgentPlugin } from "@dynamicagents/core";
2
- import type { QuickActionToolName } from "agents/browser/ai";
3
- import type { QuickActionBinding, QuickActionCommonOptions } from "agents/browser";
2
+ import type { BrowserBinding, BrowserRunOptions } from "agents/browser";
4
3
  export interface BrowserConfig {
5
4
  /**
6
5
  * The `BROWSER` binding. Browser Rendering is a **paid-plan** feature, and
7
6
  * `wrangler dev` needs `remote: true` to reach it.
8
7
  */
9
- binding: QuickActionBinding;
8
+ binding: BrowserBinding;
10
9
  /**
11
- * Character ceiling per result. Defaults to 20,000 — well under the SDK's own
12
- * 50,000, because a page that fills a small chat model's context window costs
13
- * it the conversation it was reading the page for.
10
+ * The `LOADER` binding, declared as `"worker_loaders": [{ "binding":
11
+ * "LOADER" }]`. Each call gets a fresh Dynamic Worker to run the model's
12
+ * code in, with no network access of its own.
14
13
  */
15
- maxChars?: number;
14
+ loader: WorkerLoader;
15
+ /** How long one call may take, in milliseconds. The SDK defaults to 60,000. */
16
+ timeoutMs?: number;
16
17
  /**
17
- * Which tools to expose. Defaults to the SDK's text-returning set; `content`
18
- * (raw HTML) stays opt-in because it is large and rarely what a model wants.
18
+ * Browser Run launch options: how long an idle browser lives, hostname
19
+ * guardrails, session recording, internal hostnames. Fixed when the browser
20
+ * starts rather than per call, and host-supplied, so the model never sees or
21
+ * chooses any of it.
19
22
  */
20
- actions?: QuickActionToolName[];
21
- /**
22
- * Browser Rendering options merged into every request — cookies, auth headers,
23
- * viewport. Host-supplied and never exposed to the model, which only ever
24
- * chooses the page and the action-specific fields.
25
- */
26
- options?: QuickActionCommonOptions;
23
+ provider?: BrowserRunOptions;
27
24
  }
28
25
  export declare function browser(config: BrowserConfig): AgentPlugin;
@@ -1,42 +1,104 @@
1
1
  import { definePlugin } from "@dynamicagents/core";
2
- import { createQuickActionTools } from "agents/browser/ai";
2
+ import { Browser, DurableBrowserSessionStore, browserRun } from "agents/browser";
3
+ import { browserTool } from "agents/browser/ai-sdk";
3
4
  /**
4
- * `@dynamicagents/plugins/browser` — read the web, via Cloudflare Browser Rendering
5
- * Quick Actions.
5
+ * `@dynamicagents/plugins/browser` — drive a real browser, via Cloudflare
6
+ * Browser Run.
6
7
  *
7
- * Four tools (`browser_markdown`, `browser_extract`, `browser_links`,
8
- * `browser_scrape`), the same for whichever agent installs the plugin: a parent
9
- * reads a page to answer a question directly, and a research sub-agent reads a
10
- * dozen of them.
8
+ * One tool, `browser`: the model writes JavaScript that drives a persistent
9
+ * browser over CDP, and that code runs in a Dynamic Worker with no network of
10
+ * its own. The browser outlives each call, so tabs, cookies and logins carry
11
+ * over between them, and the model never starts, closes or resets it.
11
12
  *
12
- * The binding is closed over at instantiation, so it is never model input.
13
+ * The binding and the loader are closed over at instantiation, so neither is
14
+ * ever model input.
15
+ */
16
+ /**
17
+ * The browser's name in its own store. Two `Browser` objects on one Durable
18
+ * Object need different names, so this says which one is the plugin's rather
19
+ * than taking the SDK's default and colliding with a host that also built one.
20
+ */
21
+ const BROWSER_NAME = "browser";
22
+ /**
23
+ * What the model is told about the domain. Deliberately *not* the `browser.*`
24
+ * function reference: `browserTool` puts that in the tool's own description and
25
+ * keeps it in step with the SDK, and a copy here would be a second home for
26
+ * someone else's text. What is left is what this plugin knows and the SDK
27
+ * cannot: when reaching for a browser is the right move at all.
13
28
  */
14
- /** How many characters of a page result the model is allowed to see. */
15
- const DEFAULT_MAX_CHARS = 20_000;
16
29
  const CONTEXT = [
17
- "You can read web pages directly:",
18
- "- `browser_markdown` fetches a page and returns it as Markdown. This is the default — use it first.",
19
- "- `browser_extract` pulls structured fields out of a page when you know what you are looking for.",
20
- "- `browser_links` lists a page's links, for following a trail.",
21
- "- `browser_scrape` returns the text of specific CSS selectors.",
22
- "Results are truncated to protect your context window, so prefer a specific page over a search-results page, and extract rather than reading in full when you already know the fields you need."
30
+ "You can drive a real browser by writing JavaScript, with the `browser` tool. The tool's own description carries the API; what matters in choosing it:",
31
+ "- The browser is **persistent**. A later call lands on the same page, logged in, with the same cookies. Navigate in one call and read in the next rather than rebuilding state each time.",
32
+ "- Because it is persistent, read the page before acting on it. A call that submits a form or clicks through a checkout may be a repeat of one that already succeeded, and the page in front of you is the evidence of which.",
33
+ "- Prefer one call that does several steps over several calls that each do one: every call pays a round trip to the browser, and the page is still there either way.",
34
+ "- Return the part of the page you need rather than its whole text or markup. The tool's rules say where the ceiling is.",
35
+ "- A plain HTTP fetch is cheaper than a browser. Reach for this when a page needs rendering, a session, or interaction."
23
36
  ].join("\n");
24
37
  export function browser(config) {
25
- const { binding, maxChars = DEFAULT_MAX_CHARS, actions, options } = config;
38
+ const { binding, loader, timeoutMs, provider } = config;
39
+ /**
40
+ * One `Browser` per installing object, and the lock is why.
41
+ *
42
+ * `browserTool` opens every run as a locking session, so runs in one scope
43
+ * wait for each other rather than driving one tab at once. But the SDK keeps
44
+ * its lock queues in a `WeakMap` keyed by the **`Browser` object itself**, so
45
+ * two instances over the same stored session hold two unrelated locks and
46
+ * serialise nothing. `getTools()` runs per turn, so a fresh `Browser` each
47
+ * time would leave overlapping turns in one object free to navigate each
48
+ * other's page.
49
+ *
50
+ * Keyed on the installing object's storage, which is the same granularity the
51
+ * SDK's locks have ("per isolate, which is per Durable Object"), and weak so
52
+ * an evicted object's browser handle goes with it. A `WeakMap` rather than one
53
+ * slot because nothing stops a host from hoisting this plugin to module scope
54
+ * and sharing it between objects in one isolate.
55
+ */
56
+ const browsers = new WeakMap();
57
+ const browserFor = (storage) => {
58
+ const known = browsers.get(storage);
59
+ if (known !== undefined)
60
+ return known;
61
+ const made = new Browser({
62
+ name: BROWSER_NAME,
63
+ provider: browserRun(binding, provider),
64
+ // **The store is what keeps this out of the host's Lifecycle.** A
65
+ // `Browser` with no store defaults to one over the host object's storage
66
+ // and requires `Lifecycle.use()` to install it, which a plugin has no way
67
+ // to ask for: the contract offers tools, actions, context and `requires`
68
+ // and nothing else. Passing the store explicitly is the SDK's own answer,
69
+ // and it is what lets this capability stay whole inside the plugin.
70
+ store: new DurableBrowserSessionStore(storage)
71
+ });
72
+ browsers.set(storage, made);
73
+ return made;
74
+ };
26
75
  return definePlugin({
27
76
  name: "browser",
28
- // Built per call: the SDK returns a fresh ToolSet each time, and a tool
29
- // object is not obviously safe to share across two concurrent executions.
30
- tools: () => createQuickActionTools({
31
- browser: binding,
32
- maxChars,
33
- ...(actions ? { actions } : {}),
34
- ...(options ? { options } : {})
77
+ // **No `scope`.** A scope separates callers sharing one browser, and there
78
+ // is only ever one here: the record lives in the installing object's own
79
+ // storage, a parent object is keyed by its caller, and a sub-agent runs in
80
+ // its own facet with storage of its own. Naming a scope would imply an
81
+ // isolation the store already provides, and the default one still locks.
82
+ //
83
+ // **Nothing truncates here**, and the tool is handed over as the SDK built
84
+ // it. `browserTool` bounds its own output in two places: `execute` cuts the
85
+ // result and the error *structurally*, leaving valid JSON of the original
86
+ // shape with the largest values cut first and markers where content went,
87
+ // and `toModelOutput` then redacts base64 payloads, renders a screenshot as
88
+ // a note, and bounds the logs the same way. Wrapping either to add a flat
89
+ // ceiling of our own would collapse that JSON into a string, which is
90
+ // strictly worse than the bound it would be enforcing.
91
+ tools: (ctx) => ({
92
+ browser: browserTool({
93
+ browser: browserFor(ctx.storage),
94
+ loader,
95
+ ...(timeoutMs === undefined ? {} : { timeoutMs })
96
+ })
35
97
  }),
36
98
  context: [{ provider: { get: async () => CONTEXT } }],
37
- // A plugin cannot add its own wrangler binding, so declaring it turns a
99
+ // A plugin cannot add its own wrangler binding, so declaring them turns a
38
100
  // missing one into a startup error naming this plugin, rather than a failed
39
101
  // tool call inside a request someone is waiting on.
40
- requires: { bindings: ["BROWSER"] }
102
+ requires: { bindings: ["BROWSER", "LOADER"] }
41
103
  });
42
104
  }
@@ -12,4 +12,4 @@
12
12
  * and the capture move together. AGENTS.md's "Updating Claude Code" is the
13
13
  * procedure.
14
14
  */
15
- export declare const VERIFIED_CLAUDE_CODE_VERSION = "2.1.310";
15
+ export declare const VERIFIED_CLAUDE_CODE_VERSION = "2.1.317";
@@ -12,4 +12,4 @@
12
12
  * and the capture move together. AGENTS.md's "Updating Claude Code" is the
13
13
  * procedure.
14
14
  */
15
- export const VERIFIED_CLAUDE_CODE_VERSION = "2.1.310";
15
+ export const VERIFIED_CLAUDE_CODE_VERSION = "2.1.317";
@@ -39,7 +39,11 @@ export interface GitHubSandboxEgressOptions {
39
39
  tokenBinding: string;
40
40
  /** Absent: any repository and any work branch the token reaches. */
41
41
  push?: GitPushPolicy;
42
- /** Intercept `api.github.com` as well, so `gh` works as the deployment's account. */
42
+ /**
43
+ * Intercept `api.github.com` as well, so `gh` works as the deployment's
44
+ * account. Nothing here reads those requests, so GitHub alone bounds them:
45
+ * `./README.md`, under "Protect the repositories", says how to set that up.
46
+ */
43
47
  api?: boolean;
44
48
  }
45
49
  /** The egress rule of a sandbox whose processes use GitHub. */
@@ -74,9 +74,15 @@ async function githubEgress(call, options) {
74
74
  return githubError("no GitHub token is configured for this deployment, so nothing can be done as its account", 401);
75
75
  }
76
76
  const policy = options.push;
77
- const git = new URL(call.request.url).hostname === GIT_HOST
78
- ? gitTarget(call.target.pathname)
79
- : undefined;
77
+ const onGitHost = new URL(call.request.url).hostname === GIT_HOST;
78
+ const git = onGitHost ? gitTarget(call.target.pathname) : undefined;
79
+ // GitHub serves spellings of a git path that `gitTarget` does not read, and a
80
+ // push it cannot read is one it cannot hold to the rules below, so what it
81
+ // does not recognise never gets the token. Git LFS is on such a path: a
82
+ // sandbox that needs it has to have it recognised first.
83
+ if (onGitHost && !git) {
84
+ return githubError(`this sandbox may use ${GIT_HOST} as the deployment's account for git alone, and ${call.target.pathname} is not a path git uses`, 403);
85
+ }
80
86
  let body;
81
87
  if (git) {
82
88
  if (policy &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamicagents/plugins",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Optional, composable capabilities for a Dynamic Agent: browser rendering, a Linux container, git and pull requests on GitHub, a scratchpad, and Claude Code sessions. One subpath per plugin, so a bundle grows only with what it imports.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -75,7 +75,7 @@
75
75
  "test": "vitest run",
76
76
  "test:watch": "vitest",
77
77
  "verify:exports": "node scripts/verify-exports.mjs",
78
- "verify:references": "node scripts/verify-references.mjs",
78
+ "verify:references": "da-verify-references",
79
79
  "verify:peer-ranges": "node scripts/verify-peer-ranges.mjs",
80
80
  "probe:claude-code": "npm run build && node scripts/probe-claude-code.mjs",
81
81
  "probe:github-mcp": "node scripts/probe-github-mcp.mjs",
@@ -84,12 +84,13 @@
84
84
  "prepare": "husky || true && npm run build"
85
85
  },
86
86
  "peerDependencies": {
87
- "@cloudflare/computer": "^0.5.1",
87
+ "@cloudflare/computer": "^0.8.0",
88
88
  "@cloudflare/sandbox": "^1.0.1",
89
- "@cloudflare/think": ">=0.21.0 <0.22.0",
90
- "@dynamicagents/core": ">=0.17.0 <0.18.0",
91
- "agents": ">=0.30.0 <0.31.0",
89
+ "@cloudflare/think": ">=0.23.0 <0.24.0",
90
+ "@dynamicagents/core": ">=0.18.0 <0.19.0",
91
+ "agents": ">=0.33.0 <0.34.0",
92
92
  "ai": "^7.0.88",
93
+ "just-bash": "^3.0.1",
93
94
  "zod": "^4.4.3"
94
95
  },
95
96
  "peerDependenciesMeta": {
@@ -98,16 +99,17 @@
98
99
  }
99
100
  },
100
101
  "devDependencies": {
101
- "@cloudflare/computer": "^0.5.1",
102
+ "@cloudflare/computer": "^0.8.0",
102
103
  "@cloudflare/sandbox": "^1.0.1",
103
- "@cloudflare/think": "^0.21.0",
104
+ "@cloudflare/think": "^0.23.0",
104
105
  "@cloudflare/vitest-plugin": "^1.4.4",
105
- "@dynamicagents/core": "^0.17.1",
106
+ "@dynamicagents/core": "^0.18.0",
106
107
  "@types/node": "^26.5.1",
107
- "agents": "^0.30.1",
108
+ "agents": "^0.33.0",
108
109
  "ai": "^7.0.101",
109
110
  "eslint": "^10.10.0",
110
111
  "husky": "^9.1.7",
112
+ "just-bash": "^3.0.1",
111
113
  "prettier": "^3.9.6",
112
114
  "semver": "^7.8.5",
113
115
  "typescript": "^6.0.3",