@tealbrick/kit 0.3.0-rc.6 → 0.3.0-rc.8
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/BOOTSTRAP.md +3 -3
- package/BUZZ.md +135 -0
- package/NATIVE.md +2 -2
- package/README.md +74 -2
- package/assets/buzz/buzz-backend-provider.py +99 -0
- package/assets/buzz/buzz-plist.py +22 -0
- package/assets/buzz/buzz-run.sh +39 -0
- package/assets/buzz/tb-provider-apply.py +97 -0
- package/dist/buzz.d.ts +63 -0
- package/dist/buzz.js +262 -0
- package/dist/cli.js +6 -1
- package/dist/index.d.ts +14 -14
- package/dist/native-acp.d.ts +36 -0
- package/dist/native-acp.js +219 -0
- package/dist/native-buzz.d.ts +128 -0
- package/dist/native-buzz.js +508 -0
- package/dist/native-child-guard.js +7 -2
- package/dist/native-claude.d.ts +48 -1
- package/dist/native-claude.js +311 -27
- package/dist/native-serve-config.d.ts +79 -0
- package/dist/native-serve-config.js +81 -6
- package/dist/native-serve.d.ts +1 -0
- package/dist/native-serve.js +12 -3
- package/dist/native.d.ts +1 -1
- package/dist/native.js +11 -1
- package/package.json +9 -7
package/BOOTSTRAP.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Prerelease 0.3.0-rc.
|
|
1
|
+
> Prerelease 0.3.0-rc.8 targets fresh Eve 0.70 installations. Use the explicit candidate version or `next` tag after publication. Scoped `@tealbrick/*` `latest` remains 0.2.7 for existing Eve 0.66 installations; durable cross-version session migration is not validated.
|
|
2
2
|
|
|
3
3
|
# Deployment guide: Eve agent + Teal Brick kit
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ For a fresh machine, the sequence is:
|
|
|
19
19
|
|
|
20
20
|
1. Install Node 24 and npm.
|
|
21
21
|
2. Create an Eve 0.70.0 project and configure its primary model credentials.
|
|
22
|
-
3. Install `@tealbrick/kit@0.3.0-rc.
|
|
22
|
+
3. Install `@tealbrick/kit@0.3.0-rc.8` inside that project.
|
|
23
23
|
4. Run `tealbrick setup` to sign in, select capabilities and register the card.
|
|
24
24
|
5. Build and start Eve, then check a real chat from desktop.
|
|
25
25
|
|
|
@@ -81,7 +81,7 @@ Run this inside the Eve project created in step 2 (or your existing compatible
|
|
|
81
81
|
project), not in an empty directory or as a global install:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
npm install --save-exact @tealbrick/kit@0.3.0-rc.
|
|
84
|
+
npm install --save-exact @tealbrick/kit@0.3.0-rc.8
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
This installs Portal, AVM, Voice, Vision, Deliver and the shared provider transport
|
package/BUZZ.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Run fleet agents from Buzz Desktop on a Mac host
|
|
2
|
+
|
|
3
|
+
Buzz Desktop can create a managed agent and hand it to an external provider.
|
|
4
|
+
The kit ships such a provider: Desktop (on your client Mac) deploys the agent
|
|
5
|
+
over ssh to an agent host Mac, which runs it as `buzz-acp` with
|
|
6
|
+
`tealbrick native acp` as the ACP agent. The kit owner's profile, expert
|
|
7
|
+
contracts and Portal grants stay the authority for what the agent may do.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
client Mac: Buzz Desktop -> buzz-backend-<name> --ssh--> host Mac: tb-provider-apply.py -> launchd -> buzz-run.sh -> buzz-acp -> tealbrick native acp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Client Mac: Buzz Desktop, Node 24+, `/usr/bin/python3` (Xcode Command Line Tools).
|
|
16
|
+
- Host Mac: Buzz.app installed (only its `buzz` and `buzz-acp` binaries are used),
|
|
17
|
+
Node 24+, `/usr/bin/python3`, and non-interactive ssh from the client
|
|
18
|
+
(key-based; `ssh agent-host true` must work without a prompt).
|
|
19
|
+
- Host Mac: one kit root per owner under the owners directory (default
|
|
20
|
+
`~/agents/tealbrick-connections/<owner>`), set up with
|
|
21
|
+
`tealbrick native setup` and `tealbrick native enroll`. The host helper refuses
|
|
22
|
+
owners without `<owner>/.tealbrick/native-serve.json`.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
On the host Mac:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx -y @tealbrick/kit@next buzz host setup
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On the client Mac (where Buzz Desktop runs):
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx -y @tealbrick/kit@next buzz provider install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use the same kit version on both Macs (replace `next` with an exact version to pin it).
|
|
39
|
+
The host setup installs exactly the version it runs as.
|
|
40
|
+
|
|
41
|
+
`buzz host setup` creates `~/agents/buzz/{bin,runtime}` (0700), copies `buzz`
|
|
42
|
+
and `buzz-acp` out of the app bundle into `~/agents/buzz/bin` (so a Buzz.app
|
|
43
|
+
update never changes running agents; re-run setup to pick up a new Buzz), and
|
|
44
|
+
writes `~/agents/buzz/host.json` with the node path, kit CLI, owners directory,
|
|
45
|
+
binary paths, their sha256 and the Buzz.app version. Because npx caches are
|
|
46
|
+
temporary, it installs the same kit version into `~/agents/buzz/runtime` with
|
|
47
|
+
`npm install --prefix` and points the agents at that copy. Pass
|
|
48
|
+
`--kit-cli /absolute/path/to/dist/cli.js` to use an existing install instead.
|
|
49
|
+
Re-running it is safe; it rewrites the host scripts and `host.json`.
|
|
50
|
+
|
|
51
|
+
| Flag | Default |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `--owners-dir DIR` | `~/agents/tealbrick-connections` |
|
|
54
|
+
| `--buzz-app PATH` | `/Applications/Buzz.app` |
|
|
55
|
+
| `--node PATH` | the node running setup |
|
|
56
|
+
| `--kit-cli PATH` | install into `~/agents/buzz/runtime` |
|
|
57
|
+
| `--respond-to-default owner-only\|anyone` | none (Desktop choice applies) |
|
|
58
|
+
|
|
59
|
+
`buzz provider install` writes `~/.local/bin/buzz-backend-tealbrick-mac`
|
|
60
|
+
(0755), which Desktop discovers. `--name NAME` (`[a-z0-9-]{3,40}`) changes the
|
|
61
|
+
provider id, `--bin-dir DIR` the location (it must be on Desktop's PATH or be
|
|
62
|
+
`~/.local/bin`). It refuses to overwrite a file it did not write unless
|
|
63
|
+
`--force` is given.
|
|
64
|
+
|
|
65
|
+
## Create an agent in Buzz Desktop
|
|
66
|
+
|
|
67
|
+
1. Restart Buzz Desktop after installing the provider.
|
|
68
|
+
2. Open **Agents > New agent**.
|
|
69
|
+
3. Set **Run on** to `tealbrick-mac` (shown as "Teal Brick · tealbrick-mac").
|
|
70
|
+
4. Set **owner** to the kit owner directory name on the host (for example `owner-a`).
|
|
71
|
+
5. Set **host** to the ssh alias or `user@host` of the host Mac (for example `agent-host`).
|
|
72
|
+
6. Create the agent. Desktop deploys it; the deploy waits until the agent subscribes to the relay.
|
|
73
|
+
|
|
74
|
+
The agent key goes only over ssh stdin to the host helper. It is never in a
|
|
75
|
+
command line, an environment of the provider or a log; errors redact keys.
|
|
76
|
+
On the host it is stored as `~/agents/buzz/<label>/nostr.key` (0600) and passed
|
|
77
|
+
only to that agent's `buzz-acp` process. Each agent runs in its own empty
|
|
78
|
+
environment. A failed deploy unloads its launchd unit again.
|
|
79
|
+
|
|
80
|
+
## Who the agent answers
|
|
81
|
+
|
|
82
|
+
- **Mentions:** every agent gets one rule: it wakes only when its own pubkey is
|
|
83
|
+
p-tagged (an explicit @mention), by anyone. Desktop DMs p-tag the agent, so
|
|
84
|
+
DMs work; untagged channel and thread messages are ignored.
|
|
85
|
+
- **respond_to:** the Desktop choice (`owner-only`, `allowlist`, `anyone`,
|
|
86
|
+
`nobody`) applies. If the host has `~/agents/buzz/fleet-defaults.json`
|
|
87
|
+
(written by `--respond-to-default`), its value replaces only Desktop's
|
|
88
|
+
default `owner-only`; an explicit `allowlist` or `nobody` always wins.
|
|
89
|
+
- Desktop policy settings (session policy, timeouts, parallelism up to 4, and
|
|
90
|
+
similar) pass through an allowlist; anything else is dropped.
|
|
91
|
+
|
|
92
|
+
## Files and logs on the host
|
|
93
|
+
|
|
94
|
+
| Path | Content |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `~/agents/buzz/host.json` | host paths and versions from setup |
|
|
97
|
+
| `~/agents/buzz/bin/` | `buzz`, `buzz-acp`, `buzz-run.sh`, `buzz-plist.py`, `tb-provider-apply.py` |
|
|
98
|
+
| `~/agents/buzz/<label>/` | one agent: key, auth tag, owner, relay, `acp.env`, `buzz-acp.toml`, `deploy.json` |
|
|
99
|
+
| `~/agents/buzz/<label>/logs/buzz-acp.{out,err}.log` | agent logs |
|
|
100
|
+
| `~/Library/LaunchAgents/com.tealbrick.buzz-acp.<label>.plist` | launchd unit |
|
|
101
|
+
|
|
102
|
+
`<label>` is `<owner>-<first 8 hex of the agent pubkey>-<relay host>-<4 hex>`.
|
|
103
|
+
To stop an agent: `launchctl bootout gui/$(id -u)/com.tealbrick.buzz-acp.<label>`.
|
|
104
|
+
|
|
105
|
+
The agent key and auth tag are read from their files by the last shell
|
|
106
|
+
before `buzz-acp` starts; no process command line ever carries them, only
|
|
107
|
+
their paths.
|
|
108
|
+
|
|
109
|
+
If a precondition is missing (agent directory, owner kit root, relay URL,
|
|
110
|
+
auth tag or key), `buzz-run.sh` writes one `buzz-run: <label>: ...; not
|
|
111
|
+
starting` line to `buzz-acp.err.log` and exits 0. The unit keeps
|
|
112
|
+
`KeepAlive` only for unsuccessful exits, so launchd does not restart it in a
|
|
113
|
+
loop. Fix the cause, then start it again:
|
|
114
|
+
`launchctl kickstart -k gui/$(id -u)/com.tealbrick.buzz-acp.<label>`.
|
|
115
|
+
|
|
116
|
+
## Remove an agent
|
|
117
|
+
|
|
118
|
+
Buzz Desktop has no undeploy operation for provider-managed agents. Delete
|
|
119
|
+
the agent in Desktop, then on the host Mac:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx -y @tealbrick/kit@next buzz host remove <label>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
It boots out the launchd unit, deletes
|
|
126
|
+
`~/Library/LaunchAgents/com.tealbrick.buzz-acp.<label>.plist` and moves
|
|
127
|
+
`~/agents/buzz/<label>` to `~/agents/buzz/.removed/<label>-<timestamp>`
|
|
128
|
+
(0700). The moved directory still holds the agent key; delete it when you no
|
|
129
|
+
longer need it. By hand, the same steps are:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
launchctl bootout gui/$(id -u)/com.tealbrick.buzz-acp.<label>
|
|
133
|
+
rm ~/Library/LaunchAgents/com.tealbrick.buzz-acp.<label>.plist
|
|
134
|
+
rm -rf ~/agents/buzz/<label>
|
|
135
|
+
```
|
package/NATIVE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Native agent setup — Stage A candidate
|
|
2
2
|
|
|
3
|
-
Prerelease suite **0.3.0-rc.
|
|
3
|
+
Prerelease suite **0.3.0-rc.8** targets the `next` tag. The seven scoped `@tealbrick/*` packages keep `latest` at 0.2.7, which lacks this flow; the unscoped `tealbrick` facade keeps `latest` at 0.3.0-rc.1. The `tealbrick` facade forwards the existing kit CLI; it is not another SDK. Matching Portal runtime setup endpoints must be deployed before native onboarding can succeed. RC4 added `tealbrick native serve` and `native enroll` (see README.md). RC5 keeps an idle serve process's runtime acknowledged in Portal, and lets the runtime reach Portal-provisioned Knowledge with short-lived Portal grants when no local app binding exists. RC6 adds the owner-only `/tealbrick/v1/capabilities` snapshot (Portal-granted apps and Marketplace consents) used by TBD Chat → Plugins, and `tealbrick native` without a subcommand prints usage. RC7 loads versioned expert contracts (`claude.contracts`) as SDK subagents with scope = contract ceiling ∩ owner grants, pauses approval operations for the owner's payload approval in TBD, and traces every delegation.
|
|
4
4
|
|
|
5
5
|
## Install and connect
|
|
6
6
|
|
|
7
|
-
Install with `npm install --save-exact tealbrick@0.3.0-rc.
|
|
7
|
+
Install with `npm install --save-exact tealbrick@0.3.0-rc.8` (or `tealbrick@next`) once registry publication is verified. Until then, use the review tarballs with the checksum-verifying `install-candidate.mjs`.
|
|
8
8
|
|
|
9
9
|
From your native agent workspace:
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Prerelease 0.3.0-rc.
|
|
1
|
+
> Prerelease 0.3.0-rc.8 targets fresh Eve 0.70 installations. Use the explicit candidate version or `next` tag after publication. Scoped `@tealbrick/*` `latest` remains 0.2.7 for existing Eve 0.66 installations; durable cross-version session migration is not validated.
|
|
2
2
|
|
|
3
3
|
# @tealbrick/kit
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ availability, not agent permissions. Nothing is mounted by installing the kit.
|
|
|
9
9
|
Install inside an existing Eve 0.70.0 project:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install --save-exact @tealbrick/kit@0.3.0-rc.
|
|
12
|
+
npm install --save-exact @tealbrick/kit@0.3.0-rc.8
|
|
13
13
|
npx --no-install tealbrick setup
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -93,6 +93,21 @@ including a single deduplicated provider-transport installation.
|
|
|
93
93
|
|
|
94
94
|
The `avm` selection accepts the [AVM package connection/policy schema](../avm/README.md): either `{target,sudo}` or `{selectedProfile,profiles}`. Setup can select a saved profile and preserves its grants. New connections can use paired AVMM users or operator SSH. Paired mode requires a dedicated SSH identity path and a resolvable hostname; no credentials are copied. Use `tealbrick apply config.json` for full workspace, ownership, sharing, pool and ingress grants. Disabling AVM removes its entire mount.
|
|
95
95
|
|
|
96
|
+
## Buzz Desktop agents on a Mac host: `tealbrick buzz`
|
|
97
|
+
|
|
98
|
+
Buzz Desktop can deploy a managed agent to another Mac that runs it as
|
|
99
|
+
`buzz-acp` + `tealbrick native acp` for a kit owner. Two commands set it up:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npx -y @tealbrick/kit@next buzz host setup # on the agent host Mac
|
|
103
|
+
npx -y @tealbrick/kit@next buzz provider install # on the Mac running Buzz Desktop
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then in Desktop: Agents > New agent > Run on `tealbrick-mac`, with the kit
|
|
107
|
+
`owner` and the ssh `host`. `tealbrick buzz host remove <label>` removes an
|
|
108
|
+
agent from the host. Prerequisites, mention and respond_to rules, flags, log
|
|
109
|
+
locations and removal are in [BUZZ.md](BUZZ.md).
|
|
110
|
+
|
|
96
111
|
## Native chat endpoint: `tealbrick native serve`
|
|
97
112
|
|
|
98
113
|
Optional. Lets the owner chat with a native **Claude Agent SDK** or **Codex**
|
|
@@ -139,6 +154,15 @@ stops the loop with one `native.runtime.sync.stopped` line. The per-turn
|
|
|
139
154
|
`Agent` delegates only to the configured `experts`, in the foreground with
|
|
140
155
|
the session model; each expert runs with its own `tools` (workspace reads,
|
|
141
156
|
`WebSearch`, `WebFetch`) and never receives Teal Brick tools.
|
|
157
|
+
- **Expert contracts** (`claude.contracts`: absolute paths to
|
|
158
|
+
`tealbrick.expert-contract/v1` JSON files, e.g. from the Agent-creation
|
|
159
|
+
cookbook) run as SDK subagents. Effective scope = contract ceiling ∩ the
|
|
160
|
+
owner's Portal grants ∩ the owner's `tealbrickCalls` ceiling; experts never
|
|
161
|
+
hold credentials or delegate. Payload approvals: `approvalOperations`
|
|
162
|
+
(default `marketplace_execute`) and a contract's outward writes pause the
|
|
163
|
+
exact call as an `input.requested` approval in TBD; decline or timeout denies.
|
|
164
|
+
Each delegation is traced to `.tealbrick/native-serve/traces/*.jsonl`
|
|
165
|
+
(owner, expert@version, brief, capabilities used, output digest).
|
|
142
166
|
- `GET /tealbrick/v1/capabilities` (same owner auth) reports what Portal
|
|
143
167
|
currently grants this agent: Knowledge apps from the background runtime sync
|
|
144
168
|
and, when `tealbrick native marketplace-enable` is set, Marketplace consents
|
|
@@ -153,6 +177,54 @@ stops the loop with one `native.runtime.sync.stopped` line. The per-turn
|
|
|
153
177
|
CA settings, `envPassthrough`) and never `TEALBRICK_*` values. Logs carry ids,
|
|
154
178
|
tool names and status codes, never prompts, tool inputs or results.
|
|
155
179
|
|
|
180
|
+
### Sandboxed workspace (`claude.workspace`, opt-in)
|
|
181
|
+
|
|
182
|
+
With `"workspace": {"dir": "/abs/root/work", "network": {"allowedDomains": [...]}, "envAllow": [...]}`
|
|
183
|
+
(inside the kit root; never `.tealbrick` or `inbox`), Restricted sessions become
|
|
184
|
+
**Workspace (sandboxed)** sessions:
|
|
185
|
+
|
|
186
|
+
- Tools: `Bash` (always in the Claude Code OS sandbox; `dangerouslyDisableSandbox`
|
|
187
|
+
and background commands are refused), `WebSearch`, `WebFetch`, `Agent` and the
|
|
188
|
+
Teal Brick tools. There is **no** `Read`, `Write`, `Edit`, `Glob`, `Grep`,
|
|
189
|
+
`NotebookEdit` or other in-process file tool: those run outside the sandbox
|
|
190
|
+
(symlink and check/use races), so they are removed, listed in
|
|
191
|
+
`disallowedTools` and denied by the tool gate. The model does all file work
|
|
192
|
+
with shell commands. Experts and contracts get their tools intersected with
|
|
193
|
+
this set, so they lose their file tools in this mode too.
|
|
194
|
+
- `WebFetch` refuses non-http(s) URLs, `localhost`, `*.local`, `*.localhost`,
|
|
195
|
+
`*.internal`, `*.ts.net`, single-label names and loopback, private,
|
|
196
|
+
link-local and CGNAT (100.64/10) IP literals (IPv4 and IPv6).
|
|
197
|
+
- Sandbox filesystem: writes only to the workspace (and its `.tmp`, used as
|
|
198
|
+
`TMPDIR`; git uses `.tmp/gitconfig` as its global config). Reads are denied for
|
|
199
|
+
the SDK process's `HOME`, the owners directory (the kit root's parent),
|
|
200
|
+
`<root>/.tealbrick`, `/Volumes` and `/private/var/folders`; `allowRead`
|
|
201
|
+
re-opens only the workspace, the attachment inbox, the node install prefix,
|
|
202
|
+
Claude Code's `shell-snapshots` and present `/opt/homebrew/{bin,lib,include,share,opt,Cellar}`.
|
|
203
|
+
The SDK documents that `allowRead` takes precedence over `denyRead` for
|
|
204
|
+
matching paths; a unit test pins the generated options.
|
|
205
|
+
- Every `envPassthrough` name is removed from sandboxed commands
|
|
206
|
+
(`credentials.envVars` deny) unless listed in `envAllow`; `BUZZ_*` keys,
|
|
207
|
+
`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` and `TEALBRICK_*` are always removed.
|
|
208
|
+
The SDK keeps the host's Claude Code sign-in (`CLAUDE_CONFIG_DIR` is not changed).
|
|
209
|
+
- Buzz attachments (`native acp`) are downloaded by the kit into `<root>/inbox`
|
|
210
|
+
(0700, readable but not writable by the sandbox); the model copies them into
|
|
211
|
+
the workspace with Bash.
|
|
212
|
+
- macOS tools that write to the per-user system temp folder instead of `TMPDIR`
|
|
213
|
+
(for example `sips`) fail in the sandbox, because `/private/var/folders` is
|
|
214
|
+
shared by every owner on the host and stays denied. Install workspace-local
|
|
215
|
+
tools instead (a `.venv` with Pillow, or `npm install sharp`); PyPI and the npm
|
|
216
|
+
registry need to be in `allowedDomains`.
|
|
217
|
+
- Owners on one host run as one OS user. Claude Code's sandbox points Bash `TMPDIR`
|
|
218
|
+
at `/tmp/claude-<uid>`, where every session also keeps its tool output (one
|
|
219
|
+
folder per working directory). The kit makes that root unreadable except this
|
|
220
|
+
owner's own folder, write-denies the other owners' folders (it stays writable
|
|
221
|
+
for Claude Code's own cwd tracking), and rewrites each sandboxed Bash command to
|
|
222
|
+
use the workspace `.tmp` as `TMPDIR`/`TMP`/`TEMP`.
|
|
223
|
+
- No git inside the workspace: Claude Code's sandbox refuses writes to `.git/config`
|
|
224
|
+
and `.git/hooks`, and the kit does not work around it. Version an owner's
|
|
225
|
+
workspace from outside the sandbox (for example a host job that snapshots and
|
|
226
|
+
commits it).
|
|
227
|
+
|
|
156
228
|
### Configure
|
|
157
229
|
|
|
158
230
|
`.tealbrick/native-serve.json` (owner-only, `chmod 600`) holds no secrets:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# tealbrick-buzz-provider: generated by `tealbrick buzz provider install`; safe to overwrite with --force.
|
|
3
|
+
"""Buzz Desktop managed-agent provider: runs a Desktop-managed agent on a Mac host as buzz-acp + Teal Brick
|
|
4
|
+
kit `native acp` (the owner's kit profile, expert contracts and Portal grants stay the authority).
|
|
5
|
+
Protocol v1: one JSON request on stdin, one JSON response on stdout. The agent key travels only over
|
|
6
|
+
stdin -> ssh stdin -> host helper stdin; never argv, env or logs."""
|
|
7
|
+
import hashlib, json, os, re, subprocess, sys
|
|
8
|
+
PROVIDER_ID = '__TB_PROVIDER_ID__'; VERSION = '__TB_KIT_VERSION__'
|
|
9
|
+
# Test-only override (a fake ssh); Desktop never sets it. Real deployments always use the system ssh.
|
|
10
|
+
SSH = os.environ.get('TEALBRICK_BUZZ_PROVIDER_TEST_SSH') or '/usr/bin/ssh'
|
|
11
|
+
HELPER = 'agents/buzz/bin/tb-provider-apply.py' # relative to the remote HOME (ssh starts there)
|
|
12
|
+
HEX = re.compile(r'^[0-9a-f]{64}$')
|
|
13
|
+
# --- bech32 (NIP-19 nsec) + secp256k1 x-only pubkey, stdlib only ---------------------------------
|
|
14
|
+
CS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
|
15
|
+
def nsec_hex(s):
|
|
16
|
+
s = s.strip().lower()
|
|
17
|
+
if HEX.match(s): return s
|
|
18
|
+
if not s.startswith('nsec1') or any(c not in CS for c in s[5:]): raise ValueError('private key is not nsec/hex')
|
|
19
|
+
data = [CS.index(c) for c in s[5:-6]]; acc = bits = 0; out = []
|
|
20
|
+
for v in data:
|
|
21
|
+
acc = (acc << 5) | v; bits += 5
|
|
22
|
+
while bits >= 8: bits -= 8; out.append((acc >> bits) & 0xff)
|
|
23
|
+
if len(out) != 32: raise ValueError('bad nsec length')
|
|
24
|
+
return bytes(out).hex()
|
|
25
|
+
P = 2**256 - 2**32 - 977; N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
26
|
+
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
|
|
27
|
+
def add(a, b):
|
|
28
|
+
if a is None: return b
|
|
29
|
+
if b is None: return a
|
|
30
|
+
if a[0] == b[0] and (a[1] + b[1]) % P == 0: return None
|
|
31
|
+
l = (3 * a[0] * a[0] * pow(2 * a[1], -1, P)) % P if a == b else ((b[1] - a[1]) * pow(b[0] - a[0], -1, P)) % P
|
|
32
|
+
x = (l * l - a[0] - b[0]) % P; return (x, (l * (a[0] - x) - a[1]) % P)
|
|
33
|
+
def pubkey(sk_hex):
|
|
34
|
+
k = int(sk_hex, 16)
|
|
35
|
+
if not 0 < k < N: raise ValueError('bad key')
|
|
36
|
+
r, q = None, G
|
|
37
|
+
while k:
|
|
38
|
+
if k & 1: r = add(r, q)
|
|
39
|
+
q = add(q, q); k >>= 1
|
|
40
|
+
return format(r[0], '064x')
|
|
41
|
+
# -------------------------------------------------------------------------------------------------
|
|
42
|
+
CONFIG_SCHEMA = {'type': 'object', 'required': ['owner', 'host'], 'additionalProperties': False, 'properties': {
|
|
43
|
+
'owner': {'type': 'string', 'title': 'Teal Brick kit owner on the host',
|
|
44
|
+
'description': 'Kit owner directory name under the host owners directory (set up with tealbrick native setup + native enroll), e.g. owner-a'},
|
|
45
|
+
'host': {'type': 'string', 'title': 'SSH host',
|
|
46
|
+
'description': 'SSH host alias or user@host of the agent host Mac (key-based, non-interactive), e.g. agent-host'}}}
|
|
47
|
+
def rules_toml():
|
|
48
|
+
# One rule: an agent wakes only when its own pubkey is p-tagged (an explicit @mention), by anyone.
|
|
49
|
+
# Desktop DMs p-tag the agent, so DMs still work; untagged channel and thread messages are ignored.
|
|
50
|
+
kinds = '[9, 40003, 46010, 40007]'
|
|
51
|
+
return (f'# Generated by buzz-backend-{PROVIDER_ID}: wake only on an explicit @mention (p-tag) from anyone.\n'
|
|
52
|
+
f'[[rules]]\nname = "mentions"\nchannels = "all"\nkinds = {kinds}\nrequire_mention = true\n')
|
|
53
|
+
def deploy(req):
|
|
54
|
+
agent = req.get('agent') or {}; cfg = req.get('provider_config') or {}
|
|
55
|
+
owner = str(cfg.get('owner') or '').strip().lower(); host = str(cfg.get('host') or '').strip()
|
|
56
|
+
if not re.fullmatch(r'[a-z0-9-]{2,32}', owner): raise ValueError('provider_config.owner is required (e.g. owner-a)')
|
|
57
|
+
# A leading letter/digit keeps the host from ever being parsed as an ssh option.
|
|
58
|
+
if not re.fullmatch(r'([A-Za-z0-9][A-Za-z0-9._-]{0,31}@)?[A-Za-z0-9][A-Za-z0-9._-]{0,63}', host):
|
|
59
|
+
raise ValueError('provider_config.host is required (an ssh alias or user@host, e.g. agent-host)')
|
|
60
|
+
sk = nsec_hex(agent.get('private_key_nsec') or ''); apub = pubkey(sk)
|
|
61
|
+
launch = agent.get('launch') or {}; owner_pub = (launch.get('owner_pubkey') or '').lower()
|
|
62
|
+
tag = agent.get('auth_tag')
|
|
63
|
+
if not HEX.match(owner_pub): raise ValueError('payload has no owner_pubkey')
|
|
64
|
+
if not tag: raise ValueError('agent has no NIP-OA auth_tag (recreate it in Desktop)')
|
|
65
|
+
t = json.loads(tag) if isinstance(tag, str) else tag
|
|
66
|
+
if not (isinstance(t, list) and len(t) == 4 and t[0] == 'auth' and t[1] == owner_pub): raise ValueError('auth_tag owner does not match the Desktop owner')
|
|
67
|
+
relay = (agent.get('relay_url') or '').rstrip('/')
|
|
68
|
+
m = re.fullmatch(r'wss?://([A-Za-z0-9-]+)[A-Za-z0-9.-]*(:\d+)?', relay)
|
|
69
|
+
if not m: raise ValueError('payload relay_url is not a ws(s) URL')
|
|
70
|
+
slug = m.group(1).lower()[:20] + '-' + hashlib.sha256(relay.encode()).hexdigest()[:4]
|
|
71
|
+
label = f'{owner}-{apub[:8]}-{slug}'
|
|
72
|
+
remote = {'label': label, 'owner': owner, 'relay_url': relay, 'agent_pubkey': apub, 'owner_pubkey': owner_pub,
|
|
73
|
+
'private_key': sk, 'auth_tag': json.dumps(t), 'respond_to': agent.get('respond_to'),
|
|
74
|
+
'respond_to_allowlist': agent.get('respond_to_allowlist') or [], 'name': agent.get('name'),
|
|
75
|
+
'policy_env': launch.get('policy_env') or {}, 'rules_toml': rules_toml()}
|
|
76
|
+
p = subprocess.run([SSH, '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '--', host, '/usr/bin/python3', HELPER],
|
|
77
|
+
input=json.dumps(remote), capture_output=True, text=True, timeout=180)
|
|
78
|
+
lines = [l for l in p.stdout.splitlines() if l.strip().startswith('{')]
|
|
79
|
+
try: res = json.loads(lines[-1]) if lines else None
|
|
80
|
+
except ValueError: res = None
|
|
81
|
+
if not isinstance(res, dict): res = {'ok': False, 'error': (p.stderr or 'no response from host')[-300:]}
|
|
82
|
+
if not res.get('ok'): raise RuntimeError(f"{host}: {res.get('error')}")
|
|
83
|
+
return {'ok': True, 'agent_id': f'{PROVIDER_ID}:{host}:{label}'}
|
|
84
|
+
def redact(text):
|
|
85
|
+
return re.sub(r'nsec1[0-9a-z]+|[0-9a-fA-F]{64}', '<redacted>', text)
|
|
86
|
+
def main():
|
|
87
|
+
try:
|
|
88
|
+
req = json.loads(sys.stdin.read() or '{}'); op = req.get('op')
|
|
89
|
+
if op == 'info':
|
|
90
|
+
res = {'ok': True, 'name': f'Teal Brick · {PROVIDER_ID}', 'version': VERSION, 'protocol_version': 1,
|
|
91
|
+
'description': 'Runs the agent on a Mac host over ssh as buzz-acp + Teal Brick kit native acp for a kit owner; Portal grants and kit contracts stay the authority.',
|
|
92
|
+
'config_schema': CONFIG_SCHEMA}
|
|
93
|
+
elif op == 'deploy': res = deploy(req)
|
|
94
|
+
else: raise ValueError(f'unsupported op {op!r}')
|
|
95
|
+
print(json.dumps(res)); return 0
|
|
96
|
+
except Exception as e:
|
|
97
|
+
msg = redact(f'{type(e).__name__}: {e}')[:400]
|
|
98
|
+
print(json.dumps({'ok': False, 'error': msg})); print(msg, file=sys.stderr); return 1
|
|
99
|
+
if __name__ == '__main__': sys.exit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# tealbrick-buzz-host: installed by `tealbrick buzz host setup`; rewritten on every setup run.
|
|
3
|
+
"""Writes ~/Library/LaunchAgents/com.tealbrick.buzz-acp.<label>.plist for one provider-managed agent.
|
|
4
|
+
The unit runs ~/agents/buzz/bin/buzz-run.sh <label>; logs stay in the agent directory (0700)."""
|
|
5
|
+
import os, plistlib, re, sys
|
|
6
|
+
def main():
|
|
7
|
+
if len(sys.argv) != 2 or not re.fullmatch(r'[a-z0-9][a-z0-9-]{2,62}', sys.argv[1]):
|
|
8
|
+
print('usage: buzz-plist.py <label>', file=sys.stderr); return 2
|
|
9
|
+
label = sys.argv[1]; H = os.path.expanduser('~'); BUZZ = os.path.join(H, 'agents', 'buzz')
|
|
10
|
+
D = os.path.join(BUZZ, label); logs = os.path.join(D, 'logs')
|
|
11
|
+
os.makedirs(logs, mode=0o700, exist_ok=True); os.chmod(logs, 0o700)
|
|
12
|
+
agents = os.path.join(H, 'Library', 'LaunchAgents'); os.makedirs(agents, exist_ok=True)
|
|
13
|
+
unit = f'com.tealbrick.buzz-acp.{label}'
|
|
14
|
+
body = {'Label': unit, 'ProgramArguments': ['/bin/bash', os.path.join(BUZZ, 'bin', 'buzz-run.sh'), label],
|
|
15
|
+
'WorkingDirectory': D, 'RunAtLoad': True, 'KeepAlive': {'SuccessfulExit': False},
|
|
16
|
+
'ThrottleInterval': 30, 'ProcessType': 'Background', 'Umask': 63,
|
|
17
|
+
'StandardOutPath': os.path.join(logs, 'buzz-acp.out.log'), 'StandardErrorPath': os.path.join(logs, 'buzz-acp.err.log')}
|
|
18
|
+
path = os.path.join(agents, unit + '.plist'); tmp = path + '.tmp'
|
|
19
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
20
|
+
with os.fdopen(fd, 'wb') as f: plistlib.dump(body, f)
|
|
21
|
+
os.chmod(tmp, 0o600); os.replace(tmp, path); print(path); return 0
|
|
22
|
+
if __name__ == '__main__': sys.exit(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tealbrick-buzz-host: installed by `tealbrick buzz host setup`; rewritten on every setup run.
|
|
3
|
+
# Runs one provider-managed agent: buzz-acp with Teal Brick kit `native acp` as its ACP agent.
|
|
4
|
+
# Usage (from its launchd unit): buzz-run.sh <label>
|
|
5
|
+
# Each agent gets an isolated environment (env -i): nothing from the login session leaks into the
|
|
6
|
+
# agent, and one agent's keys never reach another (block/buzz#3142). The key and auth tag are read
|
|
7
|
+
# from their files by the final shell and never appear in any process argv (only their paths do).
|
|
8
|
+
# A missing precondition logs one line and exits 0: the unit uses KeepAlive SuccessfulExit=false, so
|
|
9
|
+
# launchd does not restart-loop an agent that cannot start. Fix the cause, then
|
|
10
|
+
# `launchctl kickstart -k gui/$(id -u)/com.tealbrick.buzz-acp.<label>`.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
NODE=@@NODE@@
|
|
13
|
+
KIT_CLI=@@KIT_CLI@@
|
|
14
|
+
OWNERS_DIR=@@OWNERS_DIR@@
|
|
15
|
+
BUZZ=@@BUZZ_BIN@@
|
|
16
|
+
BUZZ_ACP=@@BUZZ_ACP_BIN@@
|
|
17
|
+
a=${1:-}
|
|
18
|
+
stop() { echo "buzz-run: ${a:-<no label>}: $1; not starting (exit 0, launchd will not restart it)" >&2; exit 0; }
|
|
19
|
+
[[ "$a" =~ ^[a-z0-9][a-z0-9-]{2,62}$ ]] || stop "bad or missing label"
|
|
20
|
+
D="$HOME/agents/buzz/$a"
|
|
21
|
+
[ -d "$D" ] || stop "agent directory $D is missing"
|
|
22
|
+
O=$(cat "$D/owner" 2>/dev/null || echo "$a")
|
|
23
|
+
[[ "$O" =~ ^[a-z0-9-]{2,32}$ ]] || stop "bad owner in $D/owner"
|
|
24
|
+
R="$OWNERS_DIR/$O"
|
|
25
|
+
[ -f "$R/.tealbrick/native-serve.json" ] || stop "kit owner '$O' is not set up under $OWNERS_DIR"
|
|
26
|
+
RELAY=$(cat "$D/relay-url" 2>/dev/null || cat "$HOME/agents/buzz/relay-url" 2>/dev/null || true)
|
|
27
|
+
[ -n "$RELAY" ] || stop "no relay URL ($D/relay-url)"
|
|
28
|
+
[ -s "$D/auth-tag.json" ] || stop "no auth tag ($D/auth-tag.json)"
|
|
29
|
+
[ -s "$D/nostr.key" ] || stop "no agent key ($D/nostr.key)"
|
|
30
|
+
exec /usr/bin/env -i HOME="$HOME" USER="${USER:-$(id -un)}" LOGNAME="${LOGNAME:-$(id -un)}" \
|
|
31
|
+
PATH="$(dirname "$NODE"):/usr/bin:/bin:/usr/sbin:/sbin" LANG=en_US.UTF-8 \
|
|
32
|
+
BUZZ_RELAY_URL="$RELAY" \
|
|
33
|
+
BUZZ_ACP_AGENT_COMMAND="$NODE" BUZZ_ACP_AGENT_ARGS="$KIT_CLI,native,acp,--root,$R,--buzz-cli,$BUZZ" \
|
|
34
|
+
BUZZ_ACP_RESPOND_TO=owner-only BUZZ_ACP_SESSION_POLICY=thread BUZZ_ACP_PERMISSION_MODE=default \
|
|
35
|
+
BUZZ_ACP_MULTIPLE_EVENT_HANDLING=queue BUZZ_ACP_IDLE_TIMEOUT=1800 BUZZ_ACP_NO_BASE_PROMPT=true \
|
|
36
|
+
BUZZ_ACP_NO_MEMORY=true BUZZ_ACP_CONFIG="$D/buzz-acp.toml" RUST_LOG=info \
|
|
37
|
+
$(grep -E "^BUZZ_ACP_(SUBSCRIBE|NO_MENTION_FILTER|CHANNELS|KINDS|RESPOND_TO|RESPOND_TO_ALLOWLIST|SESSION_POLICY|IDLE_TIMEOUT|MAX_TURN_DURATION|TURN_TIMEOUT|AGENTS|HEARTBEAT_INTERVAL|DEDUP|MULTIPLE_EVENT_HANDLING|RELAY_OBSERVER|LAZY_POOL)=[A-Za-z0-9,._:-]*$" "$D/acp.env" 2>/dev/null || true) \
|
|
38
|
+
/bin/bash -c 'set -eu; BUZZ_PRIVATE_KEY=$(cat "$1"); BUZZ_AUTH_TAG=$(cat "$2"); export BUZZ_PRIVATE_KEY BUZZ_AUTH_TAG; exec "$3"' \
|
|
39
|
+
buzz-run "$D/nostr.key" "$D/auth-tag.json" "$BUZZ_ACP"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# tealbrick-buzz-host: installed by `tealbrick buzz host setup`; rewritten on every setup run.
|
|
3
|
+
"""Host side of the Teal Brick Buzz Desktop provider. Reads ONE JSON request on stdin (it carries the agent
|
|
4
|
+
key; never argv/env), materialises ~/agents/buzz/<label>/, (re)loads its launchd unit and reports health
|
|
5
|
+
as one JSON line. Idempotent: a redeploy rewrites config and restarts the unit. Host paths come from
|
|
6
|
+
~/agents/buzz/host.json (written by `tealbrick buzz host setup`)."""
|
|
7
|
+
import json, os, re, subprocess, sys, time
|
|
8
|
+
H = os.path.expanduser('~'); BUZZ = os.path.join(H, 'agents', 'buzz')
|
|
9
|
+
HEX = re.compile(r'^[0-9a-f]{64}$')
|
|
10
|
+
LAUNCHCTL = '/bin/launchctl'
|
|
11
|
+
def out(o, code=0): print(json.dumps(o)); sys.exit(code)
|
|
12
|
+
def redact(text): return re.sub(r'nsec1[0-9a-z]+|[0-9a-fA-F]{64}', '<redacted>', text)
|
|
13
|
+
def fail(msg): out({'ok': False, 'error': redact(msg)}, 1)
|
|
14
|
+
def write(path, text, mode=0o600):
|
|
15
|
+
tmp = path + '.tmp'; fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
|
16
|
+
with os.fdopen(fd, 'w') as f: f.write(text)
|
|
17
|
+
os.chmod(tmp, mode); os.replace(tmp, path)
|
|
18
|
+
SAFE = re.compile(r'^[A-Za-z0-9,._:-]*$')
|
|
19
|
+
ALLOWED_POLICY = {'BUZZ_ACP_SESSION_POLICY', 'BUZZ_ACP_IDLE_TIMEOUT', 'BUZZ_ACP_MAX_TURN_DURATION',
|
|
20
|
+
'BUZZ_ACP_TURN_TIMEOUT', 'BUZZ_ACP_HEARTBEAT_INTERVAL', 'BUZZ_ACP_DEDUP',
|
|
21
|
+
'BUZZ_ACP_MULTIPLE_EVENT_HANDLING', 'BUZZ_ACP_SUBSCRIBE', 'BUZZ_ACP_NO_MENTION_FILTER',
|
|
22
|
+
'BUZZ_ACP_CHANNELS', 'BUZZ_ACP_KINDS', 'BUZZ_ACP_RELAY_OBSERVER', 'BUZZ_ACP_LAZY_POOL', 'BUZZ_ACP_AGENTS'}
|
|
23
|
+
def host_config():
|
|
24
|
+
try:
|
|
25
|
+
with open(os.path.join(BUZZ, 'host.json')) as f: cfg = json.load(f)
|
|
26
|
+
except FileNotFoundError: fail('host is not set up: run `tealbrick buzz host setup` on the host')
|
|
27
|
+
if not isinstance(cfg, dict) or not isinstance(cfg.get('ownersDir'), str) or not os.path.isabs(cfg['ownersDir']):
|
|
28
|
+
fail('host.json has no absolute ownersDir: re-run `tealbrick buzz host setup`')
|
|
29
|
+
return cfg
|
|
30
|
+
def main():
|
|
31
|
+
req = json.loads(sys.stdin.read())
|
|
32
|
+
if not isinstance(req, dict): fail('bad request')
|
|
33
|
+
label, owner, relay = str(req.get('label') or ''), str(req.get('owner') or ''), str(req.get('relay_url') or '')
|
|
34
|
+
if not re.fullmatch(r'[a-z0-9][a-z0-9-]{2,62}', label): fail('bad label')
|
|
35
|
+
if not re.fullmatch(r'[a-z0-9-]{2,32}', owner) or not label.startswith(owner + '-'): fail('bad owner')
|
|
36
|
+
owners_dir = host_config()['ownersDir']
|
|
37
|
+
if not os.path.isfile(os.path.join(owners_dir, owner, '.tealbrick', 'native-serve.json')):
|
|
38
|
+
fail(f'unknown or unenrolled kit owner {owner!r} on this host')
|
|
39
|
+
if not re.fullmatch(r'wss?://[A-Za-z0-9.-]+(:\d+)?/?', relay): fail('bad relay_url')
|
|
40
|
+
owner_pub = str(req.get('owner_pubkey') or ''); agent_pub = str(req.get('agent_pubkey') or '')
|
|
41
|
+
if not (HEX.match(owner_pub) and HEX.match(agent_pub)): fail('bad pubkeys')
|
|
42
|
+
try: tag = json.loads(req.get('auth_tag') or '')
|
|
43
|
+
except (TypeError, ValueError): tag = None
|
|
44
|
+
if not (isinstance(tag, list) and len(tag) == 4 and tag[0] == 'auth' and tag[1] == owner_pub and all(isinstance(x, str) for x in tag)):
|
|
45
|
+
fail('bad auth_tag')
|
|
46
|
+
desktop_rt = req.get('respond_to') or 'owner-only'
|
|
47
|
+
if desktop_rt not in ('owner-only', 'allowlist', 'anyone', 'nobody'): fail('bad respond_to')
|
|
48
|
+
# Fleet default (~/agents/buzz/fleet-defaults.json, e.g. {"respond_to": "anyone"}) replaces Desktop's
|
|
49
|
+
# *default* owner-only on a membership-only relay; a stricter explicit choice (nobody/allowlist) wins.
|
|
50
|
+
try:
|
|
51
|
+
with open(os.path.join(BUZZ, 'fleet-defaults.json')) as f: fleet = json.load(f)
|
|
52
|
+
except FileNotFoundError: fleet = {}
|
|
53
|
+
if not isinstance(fleet, dict): fleet = {}
|
|
54
|
+
rt = fleet.get('respond_to') if desktop_rt == 'owner-only' and fleet.get('respond_to') in ('anyone', 'owner-only') else desktop_rt
|
|
55
|
+
req['respond_to_effective'] = rt; req['respond_to_desktop'] = desktop_rt
|
|
56
|
+
allow = [p for p in (req.get('respond_to_allowlist') or []) if isinstance(p, str) and HEX.match(p)]
|
|
57
|
+
sk = str(req.get('private_key') or '')
|
|
58
|
+
if not (HEX.match(sk) or re.fullmatch(r'nsec1[0-9a-z]{58}', sk)): fail('bad private key')
|
|
59
|
+
rules = req.get('rules_toml')
|
|
60
|
+
if not isinstance(rules, str) or len(rules) > 4096: fail('bad rules_toml')
|
|
61
|
+
D = os.path.join(BUZZ, label); os.makedirs(os.path.join(D, 'logs'), mode=0o700, exist_ok=True); os.chmod(D, 0o700)
|
|
62
|
+
write(f'{D}/nostr.key', sk + '\n'); write(f'{D}/nostr.pub', agent_pub + '\n', 0o644)
|
|
63
|
+
write(f'{D}/auth-tag.json', json.dumps(tag)); write(f'{D}/owner', owner + '\n'); write(f'{D}/relay-url', relay + '\n')
|
|
64
|
+
env = {'BUZZ_ACP_SUBSCRIBE': 'config', 'BUZZ_ACP_RESPOND_TO': rt}
|
|
65
|
+
if rt == 'allowlist': env['BUZZ_ACP_RESPOND_TO_ALLOWLIST'] = ','.join(allow)
|
|
66
|
+
policy = req.get('policy_env') or {}
|
|
67
|
+
for k, v in (policy.items() if isinstance(policy, dict) else []):
|
|
68
|
+
if k in ALLOWED_POLICY and SAFE.match(str(v)) and k != 'BUZZ_ACP_SUBSCRIBE': env[k] = str(v)
|
|
69
|
+
if 'BUZZ_ACP_AGENTS' in env: # Desktop parallelism, capped: each slot is one kit native acp process
|
|
70
|
+
env['BUZZ_ACP_AGENTS'] = str(min(4, max(1, int(env['BUZZ_ACP_AGENTS']) if env['BUZZ_ACP_AGENTS'].isdigit() else 1)))
|
|
71
|
+
write(f'{D}/acp.env', ''.join(f'{k}={v}\n' for k, v in sorted(env.items())))
|
|
72
|
+
write(f'{D}/buzz-acp.toml', rules)
|
|
73
|
+
write(f'{D}/deploy.json', json.dumps({k: req.get(k) for k in ('label', 'owner', 'relay_url', 'agent_pubkey', 'owner_pubkey', 'respond_to_desktop', 'respond_to_effective', 'name')}, indent=1))
|
|
74
|
+
subprocess.run([sys.executable, os.path.join(BUZZ, 'bin', 'buzz-plist.py'), label], check=True, capture_output=True)
|
|
75
|
+
uid = os.getuid(); svc = f'gui/{uid}/com.tealbrick.buzz-acp.{label}'
|
|
76
|
+
plist = os.path.join(H, 'Library', 'LaunchAgents', f'com.tealbrick.buzz-acp.{label}.plist')
|
|
77
|
+
log = f'{D}/logs/buzz-acp.err.log'; start = os.path.getsize(log) if os.path.exists(log) else 0
|
|
78
|
+
loaded = subprocess.run([LAUNCHCTL, 'print', svc], capture_output=True).returncode == 0
|
|
79
|
+
if loaded: subprocess.run([LAUNCHCTL, 'kickstart', '-k', svc], check=True, capture_output=True)
|
|
80
|
+
else: subprocess.run([LAUNCHCTL, 'bootstrap', f'gui/{uid}', plist], check=True, capture_output=True)
|
|
81
|
+
ansi = re.compile(r'\x1b\[[0-9;]*m'); tail = ''
|
|
82
|
+
for _ in range(40):
|
|
83
|
+
time.sleep(1)
|
|
84
|
+
if not os.path.exists(log): continue
|
|
85
|
+
with open(log, errors='replace') as f: f.seek(start); tail = ansi.sub('', f.read())
|
|
86
|
+
if 'subscribed to membership notifications' in tail or 'subscribed to channel' in tail:
|
|
87
|
+
out({'ok': True, 'label': label, 'service': svc})
|
|
88
|
+
bad = re.search(r'(?i)(relay_membership_required|auth[^\n]*(fail|denied|invalid)|ERROR[^\n]*)', tail)
|
|
89
|
+
if bad:
|
|
90
|
+
subprocess.run([LAUNCHCTL, 'bootout', svc], capture_output=True) # no crash-looping unit on a failed deploy
|
|
91
|
+
fail('harness failed: ' + bad.group(0)[:300])
|
|
92
|
+
subprocess.run([LAUNCHCTL, 'bootout', svc], capture_output=True)
|
|
93
|
+
fail('harness did not subscribe within 40s')
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
try: main()
|
|
96
|
+
except SystemExit: raise
|
|
97
|
+
except Exception as e: fail(f'{type(e).__name__}: {str(e)[:200]}')
|
package/dist/buzz.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export declare const PROVIDER_MARKER = "tealbrick-buzz-provider";
|
|
2
|
+
export declare const buzzUsage = "Usage: tealbrick buzz provider install [--name NAME] [--bin-dir DIRECTORY] [--force]\n tealbrick buzz host setup [--owners-dir DIRECTORY] [--buzz-app PATH] [--node PATH] [--kit-cli PATH] [--respond-to-default owner-only|anyone]\n tealbrick buzz host remove <label>";
|
|
3
|
+
export declare function installProvider(options?: {
|
|
4
|
+
name?: string;
|
|
5
|
+
binDir?: string;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
home?: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
path: string;
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
updated: boolean;
|
|
13
|
+
discoverable: boolean;
|
|
14
|
+
python3: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
export type HostSetupOptions = {
|
|
17
|
+
home?: string;
|
|
18
|
+
ownersDir?: string;
|
|
19
|
+
buzzApp?: string;
|
|
20
|
+
node?: string;
|
|
21
|
+
kitCli?: string;
|
|
22
|
+
respondToDefault?: 'owner-only' | 'anyone';
|
|
23
|
+
/** Installs @tealbrick/kit@version under prefix and returns the installed dist/cli.js (tests inject this). */
|
|
24
|
+
installKit?: (prefix: string, version: string) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
export declare function setupBuzzHost(options?: HostSetupOptions): Promise<{
|
|
27
|
+
scripts: string[];
|
|
28
|
+
fleetDefaults: "owner-only" | "anyone" | null;
|
|
29
|
+
owners: string[];
|
|
30
|
+
warnings: string[];
|
|
31
|
+
version: number;
|
|
32
|
+
kitVersion: string;
|
|
33
|
+
node: string;
|
|
34
|
+
kitCli: string;
|
|
35
|
+
ownersDir: string;
|
|
36
|
+
buzzBin: string;
|
|
37
|
+
buzzAcpBin: string;
|
|
38
|
+
buzzAppVersion: string;
|
|
39
|
+
buzzSha256: {
|
|
40
|
+
buzz: string;
|
|
41
|
+
'buzz-acp': string;
|
|
42
|
+
};
|
|
43
|
+
hostJson: string;
|
|
44
|
+
}>;
|
|
45
|
+
export type LaunchctlRun = (args: string[]) => Promise<number>;
|
|
46
|
+
/**
|
|
47
|
+
* Remove one provider-managed agent from this host (Buzz Desktop has no undeploy operation): boot out its
|
|
48
|
+
* launchd unit, delete the plist and move ~/agents/buzz/<label> to ~/agents/buzz/.removed/<label>-<ts>
|
|
49
|
+
* (0700; it still holds the agent key until you delete it).
|
|
50
|
+
*/
|
|
51
|
+
export declare function removeBuzzAgent(label: string, options?: {
|
|
52
|
+
home?: string;
|
|
53
|
+
uid?: number;
|
|
54
|
+
launchctl?: LaunchctlRun;
|
|
55
|
+
now?: () => Date;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
label: string;
|
|
58
|
+
service: string;
|
|
59
|
+
bootedOut: boolean;
|
|
60
|
+
plistRemoved: boolean;
|
|
61
|
+
movedTo: string | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function buzzCommand(args: string[]): Promise<void>;
|