@clearlysh/beehaven 0.8.16
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 +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clearly (clearly.sh)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# beehaven
|
|
2
|
+
|
|
3
|
+
The command line for **Beehaven** — the Clearly cloud. Command your workspace,
|
|
4
|
+
agents, canvas and apps the way a terminal commands a filesystem, through the
|
|
5
|
+
local daemon that bridges your machine (and your AI agents) to it.
|
|
6
|
+
|
|
7
|
+
Beehaven is the name of the world Clearly runs in — the cloud, the hives, the
|
|
8
|
+
daemon, the bees. It isn't a separate product you buy; it's where Clearly lives,
|
|
9
|
+
and this is how you talk to it from a shell.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Node.js **>= 20** (the CLI is a Node program; the published archive includes its
|
|
14
|
+
runtime dependencies)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
For a one-off agent session with Node.js 20 or newer, run the npm package directly:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx --yes @clearlysh/beehaven@latest guide
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`guide` works before sign-in. It explains when Clearly is useful, gives the
|
|
25
|
+
human a signup link, and shows the authenticated first artifact. `npx` uses an
|
|
26
|
+
ephemeral npm install; it does not create a Clearly account or publish a file.
|
|
27
|
+
The package also provides the `beehaven` binary for a global npm install:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @clearlysh/beehaven@latest
|
|
31
|
+
beehaven guide
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The self-contained installer remains the recommended persistent install. It does
|
|
35
|
+
not require npm to resolve dependencies on the user's machine:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -fsSL https://clearly.sh/install.sh | sh
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That fetches a versioned tarball and writes `beehaven` to `~/.local/bin`. Published
|
|
42
|
+
archives include their dependencies; older archives may require npm. Add the bin
|
|
43
|
+
directory to your `PATH` if the installer says it isn't there.
|
|
44
|
+
|
|
45
|
+
Verify:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
beehaven --version
|
|
49
|
+
beehaven doctor # environment + daemon + relay checks
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary>Working inside the monorepo instead?</summary>
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd packages/cli && npm install && npm run build && npm link
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`npm link` may not win: the installer writes `~/.local/bin/beehaven`, and that
|
|
60
|
+
directory usually precedes the npm prefix on `PATH`. Check `which beehaven` and
|
|
61
|
+
`beehaven --version` before concluding a local change didn't take.
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
Both npm and the installer ship the public CLI build. The installer downloads
|
|
65
|
+
a versioned archive from `downloads.clearly.sh` and verifies the SHA-256 in its
|
|
66
|
+
update manifest. npm verifies package integrity through the registry.
|
|
67
|
+
|
|
68
|
+
## Updates
|
|
69
|
+
|
|
70
|
+
Run `beehaven --version` to see both the version and the file being executed.
|
|
71
|
+
The CLI checks the published version on command startup at most once an hour and
|
|
72
|
+
prints a notice when it finds a newer version. This is a notice, not an interactive
|
|
73
|
+
prompt. A curl-installed CLI (`~/.beehaven/cli`) can be updated by a person or an
|
|
74
|
+
agent with `beehaven update`; it verifies the release archive's SHA-256 before
|
|
75
|
+
installing. No workspace agent login is needed for this local command.
|
|
76
|
+
An npm install updates with npm; an `npx` run can request `@latest` again.
|
|
77
|
+
|
|
78
|
+
When a curl-installed CLI owns a standalone daemon, that daemon checks at startup
|
|
79
|
+
and hourly, installs a verified update, and restarts itself. A daemon started by the
|
|
80
|
+
Mac app leaves updates to the app. Production and staging Mac apps check on launch
|
|
81
|
+
and every four hours, download in the background, and install on quit by default;
|
|
82
|
+
the tray also offers **Check for updates**. A linked development build is updated
|
|
83
|
+
by rebuilding its checkout. Clearly Dev.app also has no release updater; rebuild
|
|
84
|
+
and reinstall that app to update its bundled CLI.
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
beehaven login # authenticate (browser OAuth)
|
|
90
|
+
beehaven connect home # set the active target — your default workspace
|
|
91
|
+
beehaven agent login codex --label "my task" --client cli # identity + one work session + brief
|
|
92
|
+
beehaven actions # what can I call here?
|
|
93
|
+
beehaven call canvas-catalog '{}' # every canvas verb + its args
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Three commands cover almost everything: **connect** to a target, ask what
|
|
97
|
+
**actions** it has, **call** one. See `beehaven help` for the full surface, or
|
|
98
|
+
`beehaven help <command>` for any single verb.
|
|
99
|
+
|
|
100
|
+
## Agent avatar expressions
|
|
101
|
+
|
|
102
|
+
The local avatar is addressable by the active agent identity. Use the short
|
|
103
|
+
flags for the primary expression surface:
|
|
104
|
+
|
|
105
|
+
beehaven --mood curious
|
|
106
|
+
beehaven --emote cheer "tests passed"
|
|
107
|
+
|
|
108
|
+
Persistent moods are: neutral, happy, sad, surprised, angry, confused, curious,
|
|
109
|
+
and flirty. Lifecycle values are `wake` and `sleep`, set with `--lifecycle`; thinking and
|
|
110
|
+
celebrating are activity poses. One-shot emotes are: wave, cheer, hearts, laugh, dance, buzz, nod,
|
|
111
|
+
shake, think, surprise, and sleep. An optional reason after an emote is capped
|
|
112
|
+
at 200 characters.
|
|
113
|
+
|
|
114
|
+
`--emote think` is the one-shot reaction. The compatibility command
|
|
115
|
+
`beehaven avatar think` is the persistent thinking state.
|
|
116
|
+
|
|
117
|
+
An active agent token routes the call to that agent's avatar; without one, the
|
|
118
|
+
house avatar is used. Compatibility commands remain available:
|
|
119
|
+
|
|
120
|
+
beehaven avatar emotion curious
|
|
121
|
+
beehaven avatar react cheer "tests passed"
|
|
122
|
+
|
|
123
|
+
The daemon MCP surface exposes avatar_emote for persistent emotion and
|
|
124
|
+
avatar_react for one-shot reactions. Both surfaces require the avatar:emote
|
|
125
|
+
permission for signed agents.
|
|
126
|
+
|
|
127
|
+
See [AVATAR_EXPRESSIONS.md](../../../docs/AVATAR_EXPRESSIONS.md) for the complete
|
|
128
|
+
cross-surface contract and source-of-truth locations.
|
|
129
|
+
|
|
130
|
+
## Talking to the cloud — daemon + DO
|
|
131
|
+
|
|
132
|
+
The CLI talks to a local **daemon** that holds the relay connection to the
|
|
133
|
+
Beehaven cloud and routes RPCs to the right Durable Object.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
beehaven login # authenticate (browser OAuth) — required
|
|
137
|
+
beehaven status # connection + auth state
|
|
138
|
+
beehaven connect home # set the active target (default workspace alias)
|
|
139
|
+
beehaven actions # list callable actions on the target
|
|
140
|
+
beehaven call <action> '{…}' # invoke one
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For scripts that need more than one workspace action, the CLI also exposes the
|
|
144
|
+
server's scripting primitives without a second execution engine:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
beehaven batch @steps.json --json # {"steps":[{"action":"…","input":{}}]}
|
|
148
|
+
beehaven code-run @script.mjs --json # source file; use `-` for stdin
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`batch` runs up to 25 sequential steps (400 actions total) and is not
|
|
152
|
+
transactional; `stopOnError` defaults to true. `code-run` is a sandbox for
|
|
153
|
+
computation between workspace calls (200 calls, 20 seconds, 100k source
|
|
154
|
+
characters). Both adapters use the same active target, agent attribution,
|
|
155
|
+
permission checks and non-zero failure semantics as `beehaven call`.
|
|
156
|
+
`batch` also accepts the worker-native `{method,params}` spelling and normalizes
|
|
157
|
+
both forms before dispatch.
|
|
158
|
+
|
|
159
|
+
The daemon normally runs inside the Hive desktop app. For CI / servers / agents
|
|
160
|
+
with no desktop, run it headless:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
beehaven daemon start # spawn a headless daemon (requires a prior `beehaven login`)
|
|
164
|
+
beehaven daemon status
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Driving the canvas (agent surface)
|
|
168
|
+
|
|
169
|
+
The CLI can review and drive the Clearly editor canvas in an open browser tab —
|
|
170
|
+
the same actions a human performs in the UI, exposed to agents over the daemon.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
beehaven canvas status [id] # compact health summary (tab connected? nodes? tool?)
|
|
174
|
+
beehaven canvas perceive [id] # read the live "room": gaze, contents, focus, chrome
|
|
175
|
+
beehaven canvas actions # discover every callable editor action
|
|
176
|
+
beehaven canvas invoke <action> '{…}' # drive one action (returns the handler result)
|
|
177
|
+
beehaven canvas verify [--composition id] # self-test EVERY canvas action, then clean up after itself
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`canvas verify` creates scratch nodes, exercises each action against the live
|
|
181
|
+
tab, asserts the result, then deletes everything it created (it diffs against a
|
|
182
|
+
baseline snapshot, so it never touches pre-existing content) and restores print
|
|
183
|
+
settings. It is the smoke test for the agent/canvas surface.
|
|
184
|
+
|
|
185
|
+
## Security
|
|
186
|
+
|
|
187
|
+
- **Authentication is required.** Every cloud call carries an authenticated
|
|
188
|
+
identity; `canvas-invoke` / `canvas-perceive` reject unauthenticated callers
|
|
189
|
+
and only reach the caller's _own_ connected clients (no cross-account access).
|
|
190
|
+
- **Allowlisted actions only.** Remote canvas control dispatches by action id
|
|
191
|
+
against a fixed registry — there is no arbitrary code execution path.
|
|
192
|
+
- **Local IPC is token-guarded.** The CLI authenticates to the daemon with a
|
|
193
|
+
per-launch token from `~/.beehaven/ipc-auth`.
|
|
194
|
+
|
|
195
|
+
## Documentation
|
|
196
|
+
|
|
197
|
+
- Per-command help: `beehaven help <command>`
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT © Clearly (clearly.sh)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE READING OF `cli.ts`'s DISPATCH SWITCH — shared by every guard that needs it.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ WHY IT IS SHARED RATHER THAN COPIED (CBH2-128, and CBH2-100 one file over). Two guards had
|
|
5
|
+
* their own parser: `verb-surface.test.ts` derived the verb list to check `VERB_SURFACE` covers
|
|
6
|
+
* it, and `limit-capable.test.ts` derived case BODIES to check `LIMIT_CAPABLE` is really wired.
|
|
7
|
+
* A second copy of "what the switch contains" drifts from the first, and the whole point of both
|
|
8
|
+
* guards is that a hand-kept list beside a dispatch goes stale. A guard that goes stale the same
|
|
9
|
+
* way is worth less than no guard, because it reports a pass.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ BOTH TRAPS BELOW WERE PAID FOR ONCE ALREADY AND ARE PRESERVED VERBATIM. Read the comments
|
|
12
|
+
* before touching either function; each one describes a run where the naive version silently
|
|
13
|
+
* reported the wrong answer rather than failing.
|
|
14
|
+
*
|
|
15
|
+
* Not a `.test.ts`, so vitest's default glob does not collect it.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
/** The dispatch, as written. */
|
|
19
|
+
export const CLI_SRC = readFileSync(new URL('../cli.ts', import.meta.url), 'utf8');
|
|
20
|
+
/**
|
|
21
|
+
* The dispatch with comments removed.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ THIS PACKAGE HAS SHIPPED GUARDS THAT MATCHED THEIR OWN DOCSTRING — an assertion forbidding a
|
|
24
|
+
* shape passes because the shape is quoted in the prose explaining why it is forbidden.
|
|
25
|
+
*/
|
|
26
|
+
export const CLI_CODE = CLI_SRC
|
|
27
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
28
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1 ');
|
|
29
|
+
/**
|
|
30
|
+
* Top-level `case` labels inside the main dispatch switch — 4-space indent, ending at `default:`.
|
|
31
|
+
*
|
|
32
|
+
* ⚠⚠ TWO PASSES, AND BOTH BOUNDS MATTER.
|
|
33
|
+
*
|
|
34
|
+
* INDENT scopes to the top-level switch: `tool`, `admin` and `compose` each open a sub-switch,
|
|
35
|
+
* whose cases (`list`, `users`, `backup`, …) are dispatch labels for a DIFFERENT switch and are
|
|
36
|
+
* not verbs. An un-anchored match pulls in 36 of them.
|
|
37
|
+
*
|
|
38
|
+
* THEN every `case '…':` ON that line, because two can share one: `case 'characters': case
|
|
39
|
+
* 'moni': {`. Anchoring alone silently drops the second and makes its entry read as a ghost.
|
|
40
|
+
*
|
|
41
|
+
* ⚠ IT READS THE RAW SOURCE, deliberately — this is the list the file DECLARES, and stripping
|
|
42
|
+
* comments first would change an answer two guards already depend on. No comment in `cli.ts`
|
|
43
|
+
* carries a 4-space-indented `case '…':`, and the count assertion in `verb-surface.test.ts` is
|
|
44
|
+
* what would notice if one ever did.
|
|
45
|
+
*/
|
|
46
|
+
export function switchVerbs() {
|
|
47
|
+
const start = CLI_SRC.indexOf('\n switch (command) {');
|
|
48
|
+
const end = CLI_SRC.indexOf('\n default: {', start);
|
|
49
|
+
if (start < 0 || end < start)
|
|
50
|
+
return [];
|
|
51
|
+
return CLI_SRC.slice(start, end).split('\n')
|
|
52
|
+
.filter((line) => /^ {4}case '/.test(line))
|
|
53
|
+
.flatMap((line) => [...line.matchAll(/case '([^']+)':/g)].map((m) => m[1]));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The body a `case` actually executes, from the COMMENT-STRIPPED source.
|
|
57
|
+
*
|
|
58
|
+
* ⚠⚠ FALL-THROUGH LABELS SHARE ONE BODY, AND IGNORING THAT MAKES A DETECTOR LIE. `case
|
|
59
|
+
* 'conversations': case 'convo': case 'conv': { … }` — slicing each label to the NEXT label gives
|
|
60
|
+
* the first two a body of 21 and 13 characters containing nothing but another label, so three
|
|
61
|
+
* correctly-wired verbs read as unwired.
|
|
62
|
+
*
|
|
63
|
+
* ⚠⚠ BRACE-MATCHED, NOT SLICED TO THE NEXT `case` LABEL. Two independent things break a label
|
|
64
|
+
* scan and BOTH bit on the first run of the guard this came from:
|
|
65
|
+
* · fall-through, above;
|
|
66
|
+
* · nesting — `case 'admin'` contains its OWN switch with ~24 `case` labels, so its body was
|
|
67
|
+
* truncated at the first of them and the `values.limit` reads 300 lines down were invisible.
|
|
68
|
+
* A brace counter is immune to both. Strings are skipped so a `}` in a message cannot close it;
|
|
69
|
+
* comments are already gone.
|
|
70
|
+
*
|
|
71
|
+
* ⚠ A CONSEQUENCE WORTH KNOWING: fall-through labels return the SAME body, so a body-derived
|
|
72
|
+
* signal cannot tell `ticket` from `board`. Any caller distinguishing them must look at what the
|
|
73
|
+
* body does with `command`, not at the body alone.
|
|
74
|
+
*/
|
|
75
|
+
export function caseBody(verb) {
|
|
76
|
+
const cli = CLI_CODE;
|
|
77
|
+
const at = cli.indexOf(`case '${verb}':`);
|
|
78
|
+
if (at < 0)
|
|
79
|
+
return '';
|
|
80
|
+
const open = cli.indexOf('{', at);
|
|
81
|
+
if (open < 0)
|
|
82
|
+
return '';
|
|
83
|
+
let depth = 0;
|
|
84
|
+
for (let i = open; i < cli.length; i++) {
|
|
85
|
+
const c = cli[i];
|
|
86
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
87
|
+
for (let j = i + 1; j < cli.length; j++) {
|
|
88
|
+
if (cli[j] === '\\') {
|
|
89
|
+
j++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (cli[j] === c) {
|
|
93
|
+
i = j;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === '{')
|
|
100
|
+
depth++;
|
|
101
|
+
else if (c === '}' && --depth === 0)
|
|
102
|
+
return cli.slice(at, i + 1);
|
|
103
|
+
}
|
|
104
|
+
return cli.slice(at);
|
|
105
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PROBE THAT RUNS THE WRAP — shared by every guard that used to read it.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THREE SEPARATE GUARDS SCANNED SOURCE FOR THE ATTRIBUTION WRAP AND ALL THREE FAILED THE DAY IT
|
|
5
|
+
* MOVED, against builds where the behaviour was intact or improved. One of them had ALREADY been
|
|
6
|
+
* rewritten once for exactly that reason and recorded the lesson in its own comment — then broke
|
|
7
|
+
* the same way again. A scan cannot distinguish "this moved" from "this was deleted", so the same
|
|
8
|
+
* failure recurs for as long as the assertion is a scan.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ A CHILD PROCESS WITH A SANDBOXED `HOME`, because `agent-state.ts` resolves the agents
|
|
11
|
+
* directory from `os.homedir()` at import time — an in-process test would read the developer's own
|
|
12
|
+
* identity and pass or fail according to whether they happened to be logged in.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ IT LOADS `dist/`, not the TypeScript. That is deliberate and it is what makes this a guard on
|
|
15
|
+
* the SHIPPED artifact: the CLI users run is `dist/cli.js`. Run `npm run build` first — the repo's
|
|
16
|
+
* other child-process probes (`resolve()` in `agent-scope-persistence.test.ts`) already assume it.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
const SRC = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const DIST = join(SRC, '..', 'dist');
|
|
25
|
+
/**
|
|
26
|
+
* Run `activeAgentWrap` for real, with `agent` planted as the active identity (or nobody when it
|
|
27
|
+
* is `null`), and return the whole `{ action, input }` it produced.
|
|
28
|
+
*/
|
|
29
|
+
export function probeWrap(agent, action, input = {}, door = 'call') {
|
|
30
|
+
const home = mkdtempSync(join(tmpdir(), 'bh-wrap-'));
|
|
31
|
+
try {
|
|
32
|
+
if (agent) {
|
|
33
|
+
const dir = join(home, '.beehaven', 'agents');
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
writeFileSync(join(dir, 'tester.json'), JSON.stringify({
|
|
36
|
+
agentId: 'id-tester', kind: 'claude-code', displayName: 'tester',
|
|
37
|
+
avatarId: 'tester', userId: 'u', token: 'x', boundScopes: ['family:claude'], ...agent,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
const mod = JSON.stringify(join(DIST, 'agent-attribution.js'));
|
|
41
|
+
const out = execFileSync(process.execPath, [
|
|
42
|
+
'-e',
|
|
43
|
+
`const a=require(${mod});`
|
|
44
|
+
+ `a.setCliDoor(${JSON.stringify(door)});`
|
|
45
|
+
+ `process.stdout.write(JSON.stringify(a.activeAgentWrap(${JSON.stringify(action)},`
|
|
46
|
+
+ `${JSON.stringify(input)}, {})));`,
|
|
47
|
+
], { env: { ...process.env, HOME: home, BEEHAVEN_AGENT: 'tester' }, encoding: 'utf8' });
|
|
48
|
+
return JSON.parse(out);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
rmSync(home, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The action a call comes out as — `agent-invoke` when it was attributed, itself when it was not. */
|
|
55
|
+
export function wrapsAs(agent, action, input = {}) {
|
|
56
|
+
return probeWrap(agent, action, input).action;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* ⚠⚠ THE END-TO-END PROBE: what `rpcOnce` PUTS ON THE WIRE.
|
|
60
|
+
*
|
|
61
|
+
* `wrapsAs` exercises the helper; this exercises the CALL SITE, and the distinction is the entire
|
|
62
|
+
* ticket. The wrap was correct for months while twelve of fifteen command modules never reached
|
|
63
|
+
* it — a guard on the helper alone would have been green throughout. So this stands up a fake IPC
|
|
64
|
+
* server, points `rpcOnce` at it, and returns the frame the daemon would have received.
|
|
65
|
+
*/
|
|
66
|
+
export function probeRpcFrame(agent, action, input = {}, door = 'call',
|
|
67
|
+
/**
|
|
68
|
+
* ⚠ `persistent` DRIVES THE OTHER SHAPE — a client held across calls (`omni`, `pipe`, `watch`,
|
|
69
|
+
* the interactive shell) rather than `rpcOnce`'s connect-call-disconnect. That distinction is
|
|
70
|
+
* not cosmetic: it is what made `rpcOnce` the wrong chokepoint, and a probe that only exercised
|
|
71
|
+
* the convenience wrapper would have agreed with the wrong answer.
|
|
72
|
+
*/
|
|
73
|
+
opts = {}) {
|
|
74
|
+
const home = mkdtempSync(join(tmpdir(), 'bh-rpc-'));
|
|
75
|
+
const sock = join(home, 'ipc.sock');
|
|
76
|
+
try {
|
|
77
|
+
if (agent) {
|
|
78
|
+
const dir = join(home, '.beehaven', 'agents');
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
writeFileSync(join(dir, 'tester.json'), JSON.stringify({
|
|
81
|
+
agentId: 'id-tester', kind: 'claude-code', displayName: 'tester',
|
|
82
|
+
avatarId: 'tester', userId: 'u', token: 'x', boundScopes: ['family:claude'], ...agent,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
const ipc = JSON.stringify(join(DIST, 'ipc-client.js'));
|
|
86
|
+
const attr = JSON.stringify(join(DIST, 'agent-attribution.js'));
|
|
87
|
+
/**
|
|
88
|
+
* ⚠ `requireAuth:false` IS WHY THE FAKE SERVER IS SIX LINES. `IpcClient.connect` skips the
|
|
89
|
+
* whole token handshake on that flag; a faithful auth simulation would be a second
|
|
90
|
+
* implementation of the thing under test.
|
|
91
|
+
*/
|
|
92
|
+
const script = `
|
|
93
|
+
const net=require('net');const fs=require('fs');
|
|
94
|
+
const server=net.createServer((s)=>{
|
|
95
|
+
s.write(JSON.stringify({type:'status',clientId:'c1',daemonPid:1,requireAuth:false})+'\\n');
|
|
96
|
+
let buf='';
|
|
97
|
+
s.on('data',(d)=>{ buf+=d.toString();
|
|
98
|
+
for(const line of buf.split('\\n')){ if(!line.trim())continue;
|
|
99
|
+
let m; try{m=JSON.parse(line);}catch{continue;}
|
|
100
|
+
if(m.type==='rpc'){
|
|
101
|
+
fs.writeFileSync(${JSON.stringify(join(home, 'frame.json'))},
|
|
102
|
+
JSON.stringify({action:m.action,input:m.input}));
|
|
103
|
+
s.write(JSON.stringify({type:'rpc-result',rid:m.rid,result:{ok:true}})+'\\n');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
buf='';
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
server.listen(${JSON.stringify(sock)}, async ()=>{
|
|
110
|
+
const a=require(${attr}); a.setCliDoor(${JSON.stringify(door)});
|
|
111
|
+
const c=require(${ipc});
|
|
112
|
+
try{
|
|
113
|
+
if(${opts.persistent ? 'true' : 'false'}){
|
|
114
|
+
const client = await c.connectClient();
|
|
115
|
+
try{ await client.call(${JSON.stringify(action)}, ${JSON.stringify(input)}, 4000); }
|
|
116
|
+
finally{ client.disconnect(); }
|
|
117
|
+
} else {
|
|
118
|
+
await c.rpcOnce(${JSON.stringify(action)}, ${JSON.stringify(input)}, 4000);
|
|
119
|
+
}
|
|
120
|
+
}catch(e){}
|
|
121
|
+
server.close(); process.exit(0);
|
|
122
|
+
});`;
|
|
123
|
+
execFileSync(process.execPath, ['-e', script], {
|
|
124
|
+
env: {
|
|
125
|
+
...process.env, HOME: home, BEEHAVEN_AGENT: 'tester',
|
|
126
|
+
BEEHAVEN_IPC_SOCKET: sock, BEEHAVEN_IPC_AUTH: 'unused',
|
|
127
|
+
},
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
timeout: 20_000,
|
|
130
|
+
});
|
|
131
|
+
return JSON.parse(readFileSync(join(home, 'frame.json'), 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
rmSync(home, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ATTRIBUTION — who ran this, and through which door.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THIS MODULE EXISTS BECAUSE PER-VERB STAMPING MISSED 28 OF 47 REMOTE VERBS.
|
|
5
|
+
*
|
|
6
|
+
* `agent_activity.source` was wired by having a verb's module import `agentRpc` instead of
|
|
7
|
+
* `rpcOnce` — three modules did (`ticket`, `sheet`, `slides`), twelve did not, and nothing
|
|
8
|
+
* anywhere reported the difference. The measured consequence was not a cosmetic gap in a report:
|
|
9
|
+
* a traffic table read "all 22 PROVISIONAL verbs have zero traffic" and the recorded retirement
|
|
10
|
+
* criterion would have deleted `task` and `search` — both demonstrably working — because they
|
|
11
|
+
* emit no activity row AT ALL. **Zero measured the instrumentation, not the usage.**
|
|
12
|
+
*
|
|
13
|
+
* ⚠ THE FIX IS THE PLACEMENT, NOT THE CODE. Everything below already existed in
|
|
14
|
+
* `commands/workspace.ts`; what changed is WHO CALLS IT — the three places an rpc frame is
|
|
15
|
+
* actually built now do, so a new verb is attributed without its author having to know this file
|
|
16
|
+
* exists:
|
|
17
|
+
*
|
|
18
|
+
* · `IpcClient.call` (`ipc-client.ts`) — the transport behind `rpcOnce`, `connectClient`,
|
|
19
|
+
* and every long-lived client (`omni`, `pipe`, …)
|
|
20
|
+
* · `daemonRpc` (`daemon-client.ts`) — the SECOND general transport: `logs`, `shop`,
|
|
21
|
+
* `costs`, the `--building` gateway, the shell
|
|
22
|
+
* · `ClaudeBridge.rpc` (`claude-bridge.ts`) — dormant, but general
|
|
23
|
+
*
|
|
24
|
+
* ⚠ `rpcOnce` WAS TRIED AS THE CHOKEPOINT AND IS ONE LEVEL TOO HIGH. It is a convenience wrapper
|
|
25
|
+
* (connect → call → disconnect); every caller that HOLDS a client goes round it. That was not a
|
|
26
|
+
* theoretical objection — `commands/omni.ts` was doing exactly that, and the guard caught it.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ IT LIVES HERE rather than in `workspace.ts` only so the transports can import it without a
|
|
29
|
+
* cycle (`workspace.ts` imports `rpcOnce`). Its dependencies are `agent-state` → `agent-scope` →
|
|
30
|
+
* node builtins, and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
import { resolveActiveAgent } from './agent-state.js';
|
|
33
|
+
/**
|
|
34
|
+
* ⚠⚠ THE VERBS THAT ESTABLISH AN IDENTITY CANNOT BE WRAPPED IN ONE — a cold-start deadlock.
|
|
35
|
+
*
|
|
36
|
+
* `agentInvoke` gates every call through `_checkAgentScope`, which refuses a principal the
|
|
37
|
+
* workspace has no `agents` row for: *"unknown agent 'X'"*. An identity is per WORKSPACE — the
|
|
38
|
+
* token carries `agentId` + `userId` and no workspace, and the row lives in the workspace DO — so
|
|
39
|
+
* arriving in a new workspace you are, correctly, nobody there.
|
|
40
|
+
*
|
|
41
|
+
* The deadlock is that the way to STOP being nobody was also wrapped:
|
|
42
|
+
*
|
|
43
|
+
* · `agent-mint-token` → refused, so `agent login` silently fell through to a LOCAL mint,
|
|
44
|
+
* printed "✓ Signed in" and "◆ Workspace session open", and every later call failed.
|
|
45
|
+
* · `agent-create` → refused, because the call to create the agent ran AS that agent.
|
|
46
|
+
*
|
|
47
|
+
* Reproduced on `checkout-test-9`, which holds ZERO agents and was therefore unreachable from the
|
|
48
|
+
* CLI by any means short of dev-scope `beehaven sql`.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ THIS IS NOT A HOLE IN THE GATE. These dispatch as the ACCOUNT OWNER, exactly as they did
|
|
51
|
+
* before any agent existed, and the owner is who is entitled to create an agent in their own
|
|
52
|
+
* workspace. Attribution is not lost either — there is nothing yet to attribute to.
|
|
53
|
+
*
|
|
54
|
+
* ⚠⚠ THE MEMBERSHIP TEST IS "DOES THIS VERB ESTABLISH AN IDENTITY", NOT A NUMBER. These five all
|
|
55
|
+
* do: mint one on the account, join a workspace with it, or see what is already there. Every
|
|
56
|
+
* OTHER verb must stay wrapped, or "run it as the owner instead" becomes the universal way round
|
|
57
|
+
* the per-agent scope — which is why the guard caps the set rather than merely listing it.
|
|
58
|
+
*/
|
|
59
|
+
export const BOOTSTRAP_ACTIONS = new Set([
|
|
60
|
+
// Account-level: mint an identity, then join a workspace with it.
|
|
61
|
+
'agent-account-mint', 'agent-admit',
|
|
62
|
+
// The workspace-scoped predecessors, kept so an older flow still bootstraps.
|
|
63
|
+
'agent-mint-token', 'agent-create', 'agent-list',
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* ⚠⚠ ACTIONS THE DAEMON ANSWERS ITSELF — THEY NEVER REACH A WORKSPACE, SO THEY CANNOT BE WRAPPED.
|
|
67
|
+
*
|
|
68
|
+
* `IpcClient.call` sends `type:'rpc'`, and `ipc-server.ts` forwards every one of those to the
|
|
69
|
+
* relay — EXCEPT the eleven the daemon intercepts in its own `ipc.on('rpc')` handler
|
|
70
|
+
* (`daemon.ts`). Wrapping one of those in `agent-invoke` produces an action the daemon does not
|
|
71
|
+
* recognise, and it would be forwarded to the cloud instead of handled locally: `beehaven connect`
|
|
72
|
+
* would stop setting the target.
|
|
73
|
+
*
|
|
74
|
+
* ⚠⚠ ENUMERATED, NEVER A "HAS A COLON" RULE. `admin:store-rpc`, `admin:broadcast`,
|
|
75
|
+
* `admin:store-query`, `_rpc:catalog` and `vector:catalog.presets` are all namespaced AND all
|
|
76
|
+
* genuinely remote. A prefix heuristic would silently un-attribute every one of them, which is
|
|
77
|
+
* this ticket's own defect wearing a tidier shape.
|
|
78
|
+
*
|
|
79
|
+
* ⚠ MIRRORS `daemon.ts`. `attribution.test.ts` reads that file and fails if the two drift — a new
|
|
80
|
+
* daemon-local verb that is not listed here becomes an action wrapped in `agent-invoke` and shipped
|
|
81
|
+
* to the cloud, which fails in a way that names neither cause.
|
|
82
|
+
*/
|
|
83
|
+
export const DAEMON_LOCAL_ACTIONS = new Set([
|
|
84
|
+
'building:connect', 'building:disconnect', 'building:connection',
|
|
85
|
+
'agent:mint', 'agent:connect', 'agent:disconnect',
|
|
86
|
+
'daemon:session-status', 'daemon:app-ensure', 'daemon:app-list',
|
|
87
|
+
'daemon:app-install', 'daemon:app-uninstall',
|
|
88
|
+
]);
|
|
89
|
+
/**
|
|
90
|
+
* ⚠⚠ WHICH DOOR THIS PROCESS IS — `cli:call` when the user typed `beehaven call`, `cli:ticket`
|
|
91
|
+
* when they typed `beehaven ticket create`, and so on.
|
|
92
|
+
*
|
|
93
|
+
* It answers the one question `agent_activity` could not: whether a typed verb or the raw
|
|
94
|
+
* `call` escape hatch did the work. Nothing else can tell them apart — both arrive as the same
|
|
95
|
+
* action, from the same agent, over the same transport — so the CLI's ~103 verbs have never had
|
|
96
|
+
* a usage figure, and any retirement list is taste until this has run for a while.
|
|
97
|
+
*
|
|
98
|
+
* ⚠ A MODULE GLOBAL, AND THAT IS THE RIGHT SHAPE HERE. One CLI process handles exactly one
|
|
99
|
+
* command; the alternative is threading a door argument through every call site, which is ~169
|
|
100
|
+
* sites today and one forgotten site per verb added — i.e. a mirror, and mirrors in this repo
|
|
101
|
+
* drift. Set once in `cli.ts` from the parsed command, read once here.
|
|
102
|
+
*
|
|
103
|
+
* ⚠ IT IS THE VERB THE USER TYPED, NOT THE ACTION DISPATCHED. `beehaven ticket create` sends
|
|
104
|
+
* `ticket-create`, and labelling the door `cli:ticket-create` would make the column a second
|
|
105
|
+
* copy of `action` — it has to name the SURFACE, or the join that answers "verb or call?"
|
|
106
|
+
* cannot be written.
|
|
107
|
+
*
|
|
108
|
+
* ⚠ EMPTY UNTIL SET. A CLI path that never calls `setCliDoor` logs no door rather than a wrong
|
|
109
|
+
* one, and an unknown door must stay distinguishable from a real one in a `GROUP BY`.
|
|
110
|
+
*/
|
|
111
|
+
let CLI_DOOR = '';
|
|
112
|
+
/** Called once from `cli.ts` with the verb the user actually typed. Aliases resolve to the
|
|
113
|
+
* canonical verb by the caller, so `cd` and `connect` do not read as two surfaces. */
|
|
114
|
+
export function setCliDoor(command) {
|
|
115
|
+
const v = String(command || '').trim().toLowerCase();
|
|
116
|
+
// ⚠ Bounded and charset-checked: it lands in a column and in a `GROUP BY`, and an
|
|
117
|
+
// unconstrained caller-supplied label is how a cardinality explosion gets into a report.
|
|
118
|
+
CLI_DOOR = /^[a-z0-9][a-z0-9:_-]{0,32}$/.test(v) ? `cli:${v}` : '';
|
|
119
|
+
}
|
|
120
|
+
export function cliDoor() {
|
|
121
|
+
return CLI_DOOR;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* ⚠⚠ IS THIS PAYLOAD ALREADY ATTRIBUTED? — the idempotency test that lets the wrap move down to
|
|
125
|
+
* the transport without double-wrapping the paths that still wrap above it.
|
|
126
|
+
*
|
|
127
|
+
* Two shapes count as attributed, and the second is the one that is easy to miss:
|
|
128
|
+
*
|
|
129
|
+
* · `agent-invoke` itself.
|
|
130
|
+
* · A CROSS-DO PROXY CARRYING ONE. `cmdCall` wraps and THEN calls `resolveBuilding`, because
|
|
131
|
+
* `agent-invoke` must travel INSIDE the proxy so the gate, the activity row and the work all
|
|
132
|
+
* land in the addressed workspace. The wire shape is `{ <type>Id, action:'agent-invoke',
|
|
133
|
+
* input }` (see `gatewayEnvelope`). Wrapping that again would produce
|
|
134
|
+
* `agent-invoke(admin:team-rpc(agent-invoke(…)))` — the outer one attributing in the LOCAL
|
|
135
|
+
* workspace's log for work performed remotely, which is precisely the ordering bug the
|
|
136
|
+
* wrap-then-proxy sequence was introduced to fix.
|
|
137
|
+
*
|
|
138
|
+
* ⚠ ONE LEVEL, NEVER A LOOP. A proxy nested in a proxy is not a shape anything produces, and a
|
|
139
|
+
* recursive walk over caller-supplied input is unbounded work on a value we do not control. The
|
|
140
|
+
* worker's own `proxiedAction` makes the same call for the same reason.
|
|
141
|
+
*/
|
|
142
|
+
export function alreadyAttributed(action, input) {
|
|
143
|
+
if (action === 'agent-invoke')
|
|
144
|
+
return true;
|
|
145
|
+
if (!input || typeof input !== 'object')
|
|
146
|
+
return false;
|
|
147
|
+
return input.action === 'agent-invoke';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Wrap a call in `agent-invoke` when an agent is signed in — BEFORE the building envelope.
|
|
151
|
+
*
|
|
152
|
+
* ⚠⚠ THE ORDER IS THE WHOLE POINT. `agent-invoke` must travel INSIDE the proxy so it executes on
|
|
153
|
+
* the workspace being addressed; wrapped outside, it runs on the daemon's default DO and
|
|
154
|
+
* `_checkAgentScope` asks the wrong roster whether the agent is a member. See the note at the call
|
|
155
|
+
* site for the measurement.
|
|
156
|
+
*
|
|
157
|
+
* ⚠ IT RESOLVES THE AGENT ITSELF rather than taking one, because the wrap now happens earlier than
|
|
158
|
+
* the refusal checks in `cmdCall`. `resolveActiveAgent` is file reads only, and resolving twice is
|
|
159
|
+
* cheaper than threading state through a routing decision.
|
|
160
|
+
*
|
|
161
|
+
* ⚠ AN EXPIRED IDENTITY IS NOT WRAPPED HERE — it is REFUSED by `cmdCall`, with a message naming
|
|
162
|
+
* the re-login. Wrapping it would send a token the server rejects and turn a clear local error
|
|
163
|
+
* into a remote one.
|
|
164
|
+
*/
|
|
165
|
+
export function activeAgentWrap(action, input, values) {
|
|
166
|
+
if (alreadyAttributed(action, input))
|
|
167
|
+
return { action, input };
|
|
168
|
+
if (BOOTSTRAP_ACTIONS.has(action) || DAEMON_LOCAL_ACTIONS.has(action))
|
|
169
|
+
return { action, input };
|
|
170
|
+
let active = null;
|
|
171
|
+
try {
|
|
172
|
+
active = resolveActiveAgent(values);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { action, input };
|
|
176
|
+
}
|
|
177
|
+
if (!active || active.expired)
|
|
178
|
+
return { action, input };
|
|
179
|
+
return {
|
|
180
|
+
action: 'agent-invoke',
|
|
181
|
+
input: {
|
|
182
|
+
agentId: active.agentId,
|
|
183
|
+
action,
|
|
184
|
+
input,
|
|
185
|
+
...(active.record.sessionId ? { sessionId: active.record.sessionId } : {}),
|
|
186
|
+
// ⚠ WHICH DOOR — `cli:call` vs `cli:ticket` vs … See `cliDoor()` and
|
|
187
|
+
// `agent_activity.source`. Set here, at the ONE wrap every CLI path shares, so a new
|
|
188
|
+
// verb is labelled without its author having to know this exists.
|
|
189
|
+
...(cliDoor() ? { source: cliDoor() } : {}),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|