@byokit/herdr 0.7.0 → 0.7.2
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/CHANGELOG.md +24 -0
- package/README.md +8 -1
- package/dist/accounts.js +43 -39
- package/dist/agents.js +6 -1
- package/dist/link.d.ts +2 -2
- package/dist/link.js +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.2 (2026-10-09)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/relay 0.6.0.
|
|
8
|
+
- Dependency update: pins @byokit/link 0.8.0.
|
|
9
|
+
- FIX: (from @byokit/pair 0.9.0) a phone killed while the person at the computer decides now resumes against the pinned computer, for compact and version 1 QRs alike. `pairWithOffer` takes `onPending(grant)`, which hands over the pending grant to keep before the computer can approve this device, and `pendingGrant` accepts a compact QR given the authenticated `host` key, and `parseOffer` reads a compact QR for inspection (expiry, addresses, name, role) without one. Before, `pendingGrant` refused compact QRs, and even a version 1 pending grant never came online after a real kill. A socket that drops once the computer has taken the code now ends pairing with `unreachable` instead of trying the next address, where the spent code or ticket could only be refused as `wrong-code` or `expired`. A socket that closes while `onPending` is still saving ends pairing at once with that close; a version 1 `onPending` that never settles times out. The compact QR is unchanged: 109 characters for one relay address and the name Umer.
|
|
10
|
+
- SECURITY: (from @byokit/pair 0.9.0) pinning and approval semantics. A compact QR's pending grant pins the key the code handshake authenticated, kept after Noise message 2 and before the device's identity goes out in message 3; if saving it fails, pairing stops before the computer learns the device. A device using `onPending` says so inside the handshake, and only then does the computer keep a yes given after its socket dropped, granting only the device key that handshake authenticated; any other device that leaves still gets no grant. A computer with another key at the same address is still refused, and a version 1 `pendingGrant` refuses a `host` that differs from its QR.
|
|
11
|
+
- FIX: (from @byokit/pair 0.9.0) the pairing picture's words match what the host now prints: scan the code, or type the shown code where the phone page asks for it, with no raw address to type.
|
|
12
|
+
- FIX: (from @byokit/pair 0.9.0) the browser pairing test closes only the Chromium processes it spawned — over that browser's own private CDP pipe — instead of signalling a whole process group, so a killed or timed-out test can no longer take out or leave behind the wrong process tree. `BYOKIT_CHROME` is the single documented key that selects the test browser.
|
|
13
|
+
|
|
14
|
+
- The herdr-kit example's agent rows lead with the workspace folder and name the agent second (`shop · Pi`), question cards name folder and agent without repeating the computer, and state badges read without the sentence's full stop (`Ready for you`).
|
|
15
|
+
|
|
16
|
+
## 0.7.1 (2026-10-07)
|
|
17
|
+
|
|
18
|
+
- Dependency update: pins @byokit/relay 0.5.3.
|
|
19
|
+
- Dependency update: pins @byokit/link 0.7.1.
|
|
20
|
+
|
|
21
|
+
- `move` and `moveToAccount` no longer type env prep into the new pane: the split placement env is verified silently through `pane.get` (set vars exact, unset vars absent, fail closed), so no `unset` lines, echo probes or account folder values stay in the scrollback after a move.
|
|
22
|
+
- The replacement agent keeps the conversation's name: it starts unique while the source is still live, then takes the source's session name through `agent.rename` once the source pane closes (best-effort; a refused rename never fails the move). New panes no longer show `move-<hex>` names.
|
|
23
|
+
- The quickstart now says what to read next (the kit spec's decisions and work packages, then the two ways in
|
|
24
|
+
and agent readiness). Docs only; no typed surface changed.
|
|
25
|
+
- The `relay` option takes anything with `RelayClient`'s `notify`, so a host can hand the kit a relay it opens after `Host.open` (the client needs the open host). A `RelayClient` still fits; behaviour is unchanged.
|
|
26
|
+
- Unreleased source dependency metadata: pin @byokit/seal 0.3.0 and @byokit/relay 0.5.2; this package's existing version is not republished. Previously published consumer metadata remains unchanged.
|
|
27
|
+
- `startAgent` with a record `env` that sets `HOME` now also places the pane's XDG config, data, state and cache directories under that home (unless set), so a sign-in inside the pane stays in the task-owned home instead of the server's.
|
|
28
|
+
|
|
5
29
|
## 0.7.0 (2026-10-02)
|
|
6
30
|
|
|
7
31
|
- Includes the Muse installation, managed-kind discovery and isolated readiness FIX/SECURITY changes recorded in the unpublished 0.6.0 preparation below; none add native login qualification.
|
package/README.md
CHANGED
|
@@ -145,6 +145,10 @@ for (const q of await hd.blocked()) {
|
|
|
145
145
|
The [herdr-kit example](../../examples/herdr-kit) is the full version: pairing with a QR code and typed code, a
|
|
146
146
|
persistent grant store, and the phone page above.
|
|
147
147
|
|
|
148
|
+
Read next: [docs/runtime-kits.md](../../docs/runtime-kits.md) §11 for the decisions and the work packages this
|
|
149
|
+
kit does not have yet, and [Two ways in](#two-ways-in) plus
|
|
150
|
+
[Agent readiness](#agent-readiness) for the parts a new app picks first.
|
|
151
|
+
|
|
148
152
|
## API at a glance
|
|
149
153
|
|
|
150
154
|
| Export | What it does |
|
|
@@ -154,7 +158,7 @@ persistent grant store, and the phone page above.
|
|
|
154
158
|
| `HERDR_VERSION`, `HERDR_PROTOCOL` | The pinned Herdr release (`0.9.1`) and the protocol the kit speaks (`22`) |
|
|
155
159
|
| `words`, `agentWords`, `stateWords`, `WORDS` | Plain sentences for agent statuses and kit states |
|
|
156
160
|
| `herdrLink` (`@byokit/herdr/link`) | The host side of the `hd.*` link ops, spread into `Host.open`; scopes each grant to its workspaces and can push sealed approval notices through a relay |
|
|
157
|
-
| `serve` (`@byokit/herdr/link`) | Finds the address with `@byokit/
|
|
161
|
+
| `serve` (`@byokit/herdr/link`) | Finds the address with `@byokit/discover` and serves the link (and an optional page) on one port |
|
|
158
162
|
| `herdrDevice` (`@byokit/herdr/device`) | The phone and browser side: typed calls over a `DeviceLink` (`tree`, `startAgent`, `prompt`, `read`, `blocked`, `answer`, `events`, `terminal`, notices); no Node import |
|
|
159
163
|
| `openNotice` (`@byokit/herdr/device`) | Opens a sealed approval notice with the device's own seed |
|
|
160
164
|
| `startFakeHerdr`, `writeBinShim`, `herdrContract` (`@byokit/herdr/testing`) | The kit's stand-in Herdr, a bin shim that runs it, and the contract suite the kit passes |
|
|
@@ -220,6 +224,9 @@ and are re-read before staging. Timeout returns `busy`, approval returns `blocke
|
|
|
220
224
|
and missing sequence or invalid bounds returns `unsupported`. The person may stop the step themselves in the pane.
|
|
221
225
|
`busy: 'interrupt'` (confirmed session/terminal/seq) returns `interrupt_unsupported`, never keys or lifecycle calls.
|
|
222
226
|
Offline fixtures do not qualify real working-step handoff or native Pi moves. `StartAgent.env` applies to newly created placements; existing shells cannot receive a new env.
|
|
227
|
+
A record env with `HOME` gives the pane a task-owned home: unless the env sets them, `XDG_CONFIG_HOME`,
|
|
228
|
+
`XDG_DATA_HOME`, `XDG_STATE_HOME` and `XDG_CACHE_HOME` move under it too, so a sign-in inside the pane stays
|
|
229
|
+
there. Other server variables still reach the pane; pass `{ env, unset }` for a fully replaced environment.
|
|
223
230
|
Tokens stay on the device and are never logged. Each provider's own terms apply to how you use your plan.
|
|
224
231
|
|
|
225
232
|
For managed-folder stores that supply resume arguments and credential shedding, use `move`:
|
package/dist/accounts.js
CHANGED
|
@@ -17,52 +17,46 @@ export function createAccountPanes(ctx) {
|
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const result = await call('pane.read', { pane_id: paneId, source: 'recent_unwrapped', lines: 40,
|
|
30
|
-
format: 'text', strip_ansi: true }, Math.max(1, deadline - Date.now()));
|
|
31
|
-
const readings = String(result?.read?.text ?? '').split('\n').map((line) => line.trim())
|
|
32
|
-
.filter((line) => line.startsWith(`${marker}=`)).map((line) => line.slice(marker.length + 1));
|
|
33
|
-
if (readings.length > 0)
|
|
34
|
-
return readings.includes(folder);
|
|
35
|
-
if (Date.now() >= deadline)
|
|
36
|
-
return false;
|
|
37
|
-
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// agent.start has no env/unset/command field in the pinned protocol. Clear credentials in
|
|
45
|
-
// the shell that launches it, then verify absence (never echo credential values).
|
|
46
|
-
async function prepareEnv(paneId, set, unset, variable, timeout) {
|
|
20
|
+
// Env verification types nothing into the pane: every prep keystroke (unset lines,
|
|
21
|
+
// echo probes) stays in the new pane's scrollback where users see it after a move. The
|
|
22
|
+
// placement env rides `pane.split`, applied by the server before the shell is shown,
|
|
23
|
+
// so `pane.get` proves the new shell's launch env: every set var exact, every unset
|
|
24
|
+
// var absent. Fail closed: the split call cannot express removals, so a leaked
|
|
25
|
+
// credential in the launch env refuses the move instead of running on it.
|
|
26
|
+
// Limitation: a shell rc overriding placement env after launch is not visible here;
|
|
27
|
+
// no pinned RPC reads shell-effective env without typing into the pane.
|
|
28
|
+
async function verifyPaneEnv(paneId, set, unset, timeout) {
|
|
47
29
|
const entries = Object.entries(set);
|
|
48
30
|
if ([...entries.map(([key]) => key), ...unset].some((key) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
49
31
|
|| entries.some(([, value]) => typeof value !== 'string' || /[\r\n\0]/.test(value))
|
|
50
32
|
|| unset.some((key) => key in set))
|
|
51
33
|
return false;
|
|
52
34
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!
|
|
35
|
+
const pane = (await call('pane.get', { pane_id: paneId }, timeout))?.pane;
|
|
36
|
+
const env = pane?.env;
|
|
37
|
+
if (!pane || typeof env !== 'object' || env === null || Array.isArray(env))
|
|
56
38
|
return false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return true;
|
|
39
|
+
if (unset.some((key) => Object.hasOwn(env, key)))
|
|
40
|
+
return false;
|
|
41
|
+
return entries.every(([key, value]) => env[key] === value);
|
|
61
42
|
}
|
|
62
43
|
catch {
|
|
63
44
|
return false;
|
|
64
45
|
}
|
|
65
46
|
}
|
|
47
|
+
// The replacement keeps the conversation's user-visible name. The server rejects
|
|
48
|
+
// duplicate agent names, so it starts unique while the source is still live, then
|
|
49
|
+
// takes the source's name once that pane is closed. A failed rename keeps the
|
|
50
|
+
// unique start name and never fails the move.
|
|
51
|
+
function moveNames(sourceName, kind) {
|
|
52
|
+
const clean = (value) => {
|
|
53
|
+
const slug = String(value ?? '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-')
|
|
54
|
+
.replace(/^[-_0-9]+/, '').slice(0, 32);
|
|
55
|
+
return /^[a-z][a-z0-9_-]{0,31}$/.test(slug) ? slug : undefined;
|
|
56
|
+
};
|
|
57
|
+
const want = clean(sourceName) ?? clean(kind) ?? 'agent';
|
|
58
|
+
return { start: `${want.slice(0, 25)}-${randomBytes(3).toString('hex')}`, want };
|
|
59
|
+
}
|
|
66
60
|
async function openSignInTab(o) {
|
|
67
61
|
const { workspaceId, label, folder, ...start } = o;
|
|
68
62
|
try {
|
|
@@ -188,16 +182,17 @@ export function createAccountPanes(ctx) {
|
|
|
188
182
|
catch {
|
|
189
183
|
return rollback('start_failed');
|
|
190
184
|
}
|
|
191
|
-
if (request !== undefined
|
|
192
|
-
|
|
185
|
+
if (request !== undefined) {
|
|
186
|
+
if (!await verifyPaneEnv(paneId, request.set, request.unset ?? [], timeout))
|
|
187
|
+
return rollback('env_mismatch');
|
|
193
188
|
}
|
|
194
|
-
if (
|
|
189
|
+
else if (!await verifyPaneEnv(paneId, { ...o.env, [variable]: o.folder }, [], timeout)) {
|
|
195
190
|
return rollback('env_mismatch');
|
|
196
191
|
}
|
|
192
|
+
const names = moveNames(agent?.name, kind);
|
|
197
193
|
try {
|
|
198
|
-
// Unique names avoid colliding with the still-live source agent.
|
|
199
194
|
await ctx.startAgent({ kind, cwd: agent.foreground_cwd ?? agent.cwd ?? '.',
|
|
200
|
-
name:
|
|
195
|
+
name: names.start, place: { pane: paneId }, args, timeoutMs: timeout });
|
|
201
196
|
await call('agent.wait', { target: paneId, until: ['idle', 'done'], timeout_ms: timeout }, timeout + 5000);
|
|
202
197
|
const deadline = Date.now() + timeout;
|
|
203
198
|
for (;;) {
|
|
@@ -267,6 +262,15 @@ export function createAccountPanes(ctx) {
|
|
|
267
262
|
return failed('close_failed', live);
|
|
268
263
|
}
|
|
269
264
|
// The source is closed: a notification failure cannot undo a successful move.
|
|
265
|
+
// Take the conversation's name now that it is free; the unique start name stays
|
|
266
|
+
// when the rename is refused, and a rename failure never undoes the move.
|
|
267
|
+
if (names.want !== names.start) {
|
|
268
|
+
try {
|
|
269
|
+
await call('agent.rename', { target: paneId, name: names.want }, timeout);
|
|
270
|
+
replacement.name = names.want;
|
|
271
|
+
}
|
|
272
|
+
catch { /* the unique start name stays; the move already succeeded */ }
|
|
273
|
+
}
|
|
270
274
|
try {
|
|
271
275
|
request?.onReplaced?.(paneId);
|
|
272
276
|
}
|
package/dist/agents.js
CHANGED
|
@@ -116,7 +116,12 @@ export function createAgents(ctx) {
|
|
|
116
116
|
throw fail('env_mismatch', 'This pane needs to be opened again to use that sign-in.');
|
|
117
117
|
}
|
|
118
118
|
const timeout = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
119
|
-
|
|
119
|
+
// A pane HOME alone keeps the server's XDG dirs, so a sign-in would land outside it: confine them too.
|
|
120
|
+
const home = launch ? undefined : o.env?.HOME;
|
|
121
|
+
const env = o.env === undefined || launch ? {} : { env: home === undefined ? o.env : {
|
|
122
|
+
XDG_CONFIG_HOME: join(home, '.config'), XDG_DATA_HOME: join(home, '.local', 'share'),
|
|
123
|
+
XDG_STATE_HOME: join(home, '.local', 'state'), XDG_CACHE_HOME: join(home, '.cache'), ...o.env
|
|
124
|
+
} };
|
|
120
125
|
let paneId;
|
|
121
126
|
let created = false;
|
|
122
127
|
if (o.worktree !== undefined) {
|
package/dist/link.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import { type Grant, type Host, type HostOptions } from '@byokit/link';
|
|
3
|
-
import { type ServeIngress, type Via } from '@byokit/
|
|
3
|
+
import { type ServeIngress, type Via } from '@byokit/discover';
|
|
4
4
|
import type { RelayClient } from '@byokit/relay';
|
|
5
5
|
import type { HerdrKit } from './kit.ts';
|
|
6
6
|
export type HerdrScope = {
|
|
@@ -9,7 +9,7 @@ export type HerdrScope = {
|
|
|
9
9
|
export declare function herdrLink(kit: HerdrKit, o: {
|
|
10
10
|
scopeOf: (grant: Grant) => HerdrScope;
|
|
11
11
|
passThrough?: (method: string, grant: Grant) => boolean;
|
|
12
|
-
relay?: RelayClient
|
|
12
|
+
relay?: Pick<RelayClient, 'notify'>;
|
|
13
13
|
}): Pick<HostOptions, 'handle' | 'stream' | 'allow'>;
|
|
14
14
|
export declare function serve(o: {
|
|
15
15
|
host: Host;
|
package/dist/link.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createServer } from 'node:http';
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import { PublicLinkError, } from '@byokit/link';
|
|
10
|
-
import { reach } from '@byokit/
|
|
10
|
+
import { reach } from '@byokit/discover';
|
|
11
11
|
import { stateWords, words } from "./words.js";
|
|
12
12
|
import { decodeB64Url, sealNotice } from "./notices.js";
|
|
13
13
|
// Ops a view-role grant may open; every other op needs the control role (7.1, *view* marks).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/herdr",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Drive the Herdr on this computer — workspaces, tabs, panes, agents and blocked-approval answers — from an app, or hand it to a phone over @byokit/link.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@byokit/link": "0.
|
|
56
|
-
"@byokit/relay": "0.
|
|
57
|
-
"@byokit/
|
|
58
|
-
"@byokit/seal": "0.
|
|
59
|
-
"ws": "8.
|
|
55
|
+
"@byokit/link": "0.8.0",
|
|
56
|
+
"@byokit/relay": "0.6.0",
|
|
57
|
+
"@byokit/discover": "0.6.0",
|
|
58
|
+
"@byokit/seal": "0.3.0",
|
|
59
|
+
"ws": "8.22.0"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|