@byokit/openclaw 0.6.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +83 -6
- package/SECURITY.md +4 -3
- package/dist/auth-status.d.ts +9 -0
- package/dist/auth-status.js +66 -2
- package/dist/auth-store.d.ts +6 -4
- package/dist/auth-store.js +13 -14
- package/dist/engine-patches.js +19 -13
- package/dist/engine.js +5 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/kit.d.ts +7 -1
- package/dist/kit.js +68 -15
- package/dist/link.js +1 -17
- package/dist/members.js +13 -2
- package/dist/runs.d.ts +4 -0
- package/dist/runs.js +21 -7
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +2 -1
- package/dist/testing/contract.js +13 -0
- package/dist/testing/fake-gateway.d.ts +3 -1
- package/dist/testing/fake-gateway.js +10 -3
- package/dist/types.d.ts +16 -2
- package/dist/words.js +2 -0
- package/dist/words.json +1 -0
- package/engine/patches.json +12 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.0 (2026-10-09)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/relay 0.6.0.
|
|
8
|
+
- Dependency update: pins @byokit/link 0.8.0.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
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
|
+
- Cut first-install wall time ~3x (freeze ~20x) by dropping the 37,999 per-entry `fsync`s from `freeze()`: entries are chmodded read-only in bounded batches, one filesystem-wide `sync -f` flush runs where available (Linux, ~2 s measured vs ~110 s of per-file fsync transactions), and the manifest is read from the built tree once. Durability rule: `docs/runtime-kits.md` 5.16.
|
|
15
|
+
- FEAT: Name the engine's learning switch: `learning()`, `setLearning(mode | 'default')` and `restoreLearning(captured)` over the config key `skills.workshop.autonomous.mode` (`off | propose | auto`, engine default `auto` when absent), as a typed, file-backed veneer on the one-key pair — valid on a stopped home, before boot and after `stop()`. Absence is a value (`{ present: false }`); an unknown mode throws listing the accepted values, a stored value outside the enum makes `learning()` throw naming it, both writers throw when `KitOptions.config` also passes `skills`, and `restoreLearning` reads back and throws on mismatch. The README also documents the supported repair of a retained home killed without `stop()` (orphan gateway, stale lock, reseal, exit-78 doctor repair) and cross-references the separate auth-store seal size bound.
|
|
16
|
+
|
|
17
|
+
## 0.8.0 (2026-10-09)
|
|
18
|
+
|
|
19
|
+
- Forward actual thinking-token progress and report Gateway acceptance as `started`; reuse bounded, file-witnessed member admission status so ready runs avoid redundant sign-in checks without skipping authority checks. Device state uses the same live readiness boundary; `providerStatus` preserves unknown versus empty sign-in status.
|
|
20
|
+
- FIX: On the Claude CLI route the printed `## Tooling` list now uses the Claude wire catalog names (`mcp__openclaw__<tool>`), so a listed tool can be called exactly as printed. Every other route keeps the stock prompt byte-identical, and already-prefixed names are never prefixed again.
|
|
21
|
+
- The deprecated `anthropic` provider alias on route `anthropic-cli` stays readable as `route.deprecatedProvider` (`'anthropic'`); its removal moves to 0.9.0.
|
|
22
|
+
- FIX: `OpenClawKit.abort` always returns a Promise, including after an engine drop; cancellation failures remain catchable without changing healthy abort results.
|
|
23
|
+
- FIX: `ensureMember` resolves only once `agents.list` shows the member (bounded ~5 s wait, then a plain error naming the member), so a turn sent right after it is no longer rejected as `unknown agent id` while the engine hot-reloads its roster.
|
|
24
|
+
|
|
25
|
+
## 0.7.0 (2026-10-08)
|
|
26
|
+
|
|
27
|
+
- FIX: Unreadable sealed sign-ins now fail closed with `AuthStoreUnreadableError` and recovery words, leaving `auth-store.sealed` byte-identical for original-key or backup repair and retry instead of starting signed out with a replacement store.
|
|
28
|
+
|
|
5
29
|
## 0.6.3 (2026-10-07)
|
|
6
30
|
|
|
7
31
|
- Dependency update: pins @byokit/relay 0.5.3.
|
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ phone page that pairs, signs in with ChatGPT, runs and answers approvals, see
|
|
|
30
30
|
`engine/patches.json` declares the exact upstream tarball integrity, commit, unique edits and before/after file
|
|
31
31
|
hashes. The Gateway Workshop review patch writes content-free, durable usage facts only when the kit sets its
|
|
32
32
|
accounting environment. The separately bundled worker and all other detached kinds remain uncovered.
|
|
33
|
+
The Claude CLI route also uses the bundled [Tooling-name correction](../../docs/runtime-kits.md#516-bundled-engine-patches-and-engine-started-usage);
|
|
34
|
+
apps do not need consumer tool aliases.
|
|
33
35
|
The full upstream MIT notice ships in [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
|
|
34
36
|
|
|
35
37
|
Each `prepare()` verifies the **entire** selected engine tree, including unpatched files. The kit installs stock
|
|
@@ -130,7 +132,7 @@ For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'opena
|
|
|
130
132
|
`ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
|
|
131
133
|
`keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
|
|
132
134
|
label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
|
|
133
|
-
app state. The kit never returns engine key-check errors or the key.
|
|
135
|
+
app state. The kit never returns engine key-check errors or the key. [`@byokit/ui/kits`](../ui/README.md) supplies `keyStep`/`keyView`
|
|
134
136
|
for entry, checking, success and failure states, using the kit's `key.*` words.
|
|
135
137
|
|
|
136
138
|
To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
|
|
@@ -142,9 +144,23 @@ because the engine's profile pins can rotate. The `byokit-key-` member prefix is
|
|
|
142
144
|
replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
|
|
143
145
|
an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
|
|
144
146
|
|
|
147
|
+
`OpenClawKit.abort` always returns a Promise. Await it or attach `.catch(...)`: an unavailable engine, transport
|
|
148
|
+
failure or invalid API-key session key rejects with the original error, never throws synchronously, and is not
|
|
149
|
+
reported as successful cancellation. Healthy cancellation ends the run as defined in
|
|
150
|
+
[the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams).
|
|
151
|
+
|
|
145
152
|
A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
|
|
146
153
|
(`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
|
|
147
|
-
it.
|
|
154
|
+
it. Run events report `started` when the Gateway accepts the request (not when it finishes), and forward
|
|
155
|
+
actual `thinking` progress as `{ type: 'thinking', tokens }`. Silent routes invent no progress; cached replays
|
|
156
|
+
without a new accepted frame emit no `started`. Handle these frames explicitly rather than treating every
|
|
157
|
+
non-text/tool frame as an end.
|
|
158
|
+
Live readiness reads (`providers`, `signedIn`, or device `state()`) prepare admission for the next run, avoiding
|
|
159
|
+
redundant checks while authority remains reusable. `providerStatus(member)` returns `undefined` for unknown
|
|
160
|
+
status rather than `[]`; `providers` maps unknown to `[]`, and `signedIn` to `false`.
|
|
161
|
+
See [the admission contract](../../docs/runtime-kits.md#58-runs-members-and-streams) for lifetime, expiry,
|
|
162
|
+
file witnesses, invalidation and remote-revocation limits.
|
|
163
|
+
Tool events carry the engine's call `id`, the `input` on `start` and the `output` and `error` on `end`. A run that
|
|
148
164
|
ends ok carries `usage` (the engine's token total for the run, and `costUsd` when it priced the model) and
|
|
149
165
|
`planWindow` (the subscription's quota windows as the engine last read them), each only when the engine reports it.
|
|
150
166
|
`openclawDevice(link).run(message, o)` takes the same options over the link, and `state()` adds the kit and engine
|
|
@@ -331,7 +347,7 @@ Entries:
|
|
|
331
347
|
| `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
|
|
332
348
|
| `openclawDevice(link)` (`./device`) | Portable client: state, routes, sign-in, runs, steer, abort, approvals, events, sealed notices, pass-through `call`. `OpenClawDevice` and `DeviceEndFrame` name what it returns |
|
|
333
349
|
| `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
|
|
334
|
-
| `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds `@byokit/ui
|
|
350
|
+
| `words`, `stateWords`, `toAccountView` (`.`, `./device`) | The kit's sentences, so a phone shows the words the computer does; `toAccountView` feeds [`@byokit/ui`](../ui/README.md)'s `phaseOf` |
|
|
335
351
|
| `openclawLink(kit, o)` (`./link`) | `handle` / `stream` / `allow` for a `@byokit/link` `Host`, checked per member, plus `onAction` for relay push actions. `OpenClawLinkOptions` / `OpenClawLinkHost` name both sides |
|
|
336
352
|
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
|
|
337
353
|
| `fakeGateway`, `openclawContract`, `startModelStub`, `useModelStub` (`./testing`) | In-memory Gateway, the contract suite and the scripted model for tests. `fakeGateway`'s script is typed per method (`FakeScript`, `FakeHandler`), and a recorded `StubCall` carries a typed `StubRequest` |
|
|
@@ -375,6 +391,57 @@ back out), the write is atomic in `prepare()`'s exact shape, and the engine appl
|
|
|
375
391
|
your app does **not** also pass in `KitOptions.config`: that option is merged over the saved file on every boot and
|
|
376
392
|
wins. Don't interleave a narrow set with `patchConfig`, which rewrites the whole config through the Gateway.
|
|
377
393
|
|
|
394
|
+
## Engine learning state
|
|
395
|
+
|
|
396
|
+
The engine's learning switch is the config key `skills.workshop.autonomous.mode` (`off | propose | auto`; engine
|
|
397
|
+
default `auto` when absent). `off` keeps only the suggestion nudge, `propose` creates pending proposals, `auto`
|
|
398
|
+
applies captured proposals and runs the scanner-gated cleanup; the engine projects a system-owned cron job
|
|
399
|
+
`skill-collection-review-<agentId>` per workspace agent, enabled only on `auto`. The kit names it — file-backed like
|
|
400
|
+
the pair above, so it works on a stopped home, before boot and after `stop()`:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { OpenClawKit } from '@byokit/openclaw';
|
|
404
|
+
|
|
405
|
+
const kit = new OpenClawKit({ stateDir: './openclaw-state' });
|
|
406
|
+
|
|
407
|
+
kit.learning(); // { present: false } | { present: true, mode: 'off' | 'propose' | 'auto' }
|
|
408
|
+
const before = kit.learning(); // capture before maintenance
|
|
409
|
+
kit.setLearning('off'); // returns the state it replaced
|
|
410
|
+
kit.setLearning('default'); // remove the key: the engine default (auto) applies again
|
|
411
|
+
kit.restoreLearning(before); // writes the capture, reads it back, throws on mismatch
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Absence is a value (`{ present: false }`), never "probably default". An unknown mode throws listing the accepted
|
|
415
|
+
values (`off|propose|auto`, plus `default` for `setLearning`), a stored value outside the enum makes `learning()`
|
|
416
|
+
throw naming it, and both writers throw when the app also passes a `skills` key in `KitOptions.config` (that option
|
|
417
|
+
wins at every boot). A home that was never prepared surfaces the file read's ENOENT: `prepare()` once first.
|
|
418
|
+
|
|
419
|
+
## Retained home killed without stop()
|
|
420
|
+
|
|
421
|
+
A host crash or `kill -9` skips `stop()` cleanup. The next `prepare()` (the next `start()`) repairs the home
|
|
422
|
+
itself — no manual file surgery:
|
|
423
|
+
|
|
424
|
+
- It stops a leftover Gateway only after verifying it is a same-user orphan of this root (matching pid, identity,
|
|
425
|
+
start time, exe/cwd and env), and only when the auth-store lock's owner is dead; then it seals the leftover live
|
|
426
|
+
`state/` and `home/` trees back into the store under the acquired lock. Anything ambiguous (a live lock owner, an
|
|
427
|
+
unverifiable pid, another writer alive, shutdown timeout) fails closed with `EngineAlreadyRunningError` instead.
|
|
428
|
+
- A stale `auth-store.lock/` with a dead owner is recovered through a `recovery/` marker; interrupted
|
|
429
|
+
`cleanup`/`restoring` transitions are completed from the authenticated snapshot; empty live trees are treated as
|
|
430
|
+
debris.
|
|
431
|
+
- An engine exit code 78 means the engine asked for repair: the kit's own repair path runs
|
|
432
|
+
`doctor --fix --yes` with `doctorContext()`'s exact env.
|
|
433
|
+
|
|
434
|
+
What a killed home leaves behind: live plaintext `state/` and `home/` (never resealed), `auth-store.lock/` with a
|
|
435
|
+
dead owner pid, `gateway.pid` and `gateway.identity` (a still-live orphan Gateway is the dangerous case),
|
|
436
|
+
`openclaw.json` with its `.bak.*` journal, `engine-set`, `plugin/`, `workspaces/`, `usage/`, `tmp/`, `npm-cache/`
|
|
437
|
+
and `install-home/`. None of these is safe to edit by hand, and `auth-store.sealed` above all: never hand-edit it —
|
|
438
|
+
restore an authentic backup under the lock instead (see the credential sealing section). Keep backups before any
|
|
439
|
+
manual intervention.
|
|
440
|
+
|
|
441
|
+
If a leftover `home/` carries large regenerable tool caches, resealing can exceed the runtime string bound and
|
|
442
|
+
abort; that seal size bound is a separate, known limitation (tracked as the auth-store seal size demand), not
|
|
443
|
+
fixed by the recovery path — move only regenerable cache trees aside (keeping backups) and retry `start()`.
|
|
444
|
+
|
|
378
445
|
## App-owned task recovery
|
|
379
446
|
|
|
380
447
|
If your app requeues interrupted tasks itself, declare their namespaces **before starting the kit**:
|
|
@@ -484,8 +551,18 @@ Without `authSeal`, engine credentials remain plaintext. With it, successful `pr
|
|
|
484
551
|
only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
|
|
485
552
|
disk unsealed. The adapter authenticates the snapshot
|
|
486
553
|
before any restoration; a missing adapter rejects. A store the key cannot open (a wrong key,
|
|
487
|
-
damaged or tampered bytes) or whose payload is not a credential snapshot
|
|
488
|
-
`auth-store.sealed
|
|
554
|
+
damaged or tampered bytes) or whose payload is not a credential snapshot stays unchanged at
|
|
555
|
+
`auth-store.sealed`. Both `prepare()` and `start()` reject with exported `AuthStoreUnreadableError`
|
|
556
|
+
(`code: 'auth-store-unreadable'`, `reason: 'auth-failed' | 'invalid-snapshot'`). The kit reports
|
|
557
|
+
`{ phase: 'failed', why: 'auth-store-unreadable' }`; show `stateWords(kit.state)`, not a sign-in prompt.
|
|
558
|
+
Restore access to the original seal/key (and the original service/stateDir binding if the adapter uses
|
|
559
|
+
one), then retry `start()` with that adapter. Do not generate/rotate a key to repair an unreadable store.
|
|
560
|
+
If the bytes are damaged, stop all writers, acquire the host writer lock, and restore an authentic
|
|
561
|
+
backup with its matching key before retrying. There is no automatic reset or overwrite.
|
|
562
|
+
For stores kept aside by an older kit, restore the original `.unreadable-*` snapshot to
|
|
563
|
+
`auth-store.sealed` only while stopped under that lock, preserving both it and any replacement as
|
|
564
|
+
backups first; use the original seal. Without the matching key or an authentic backup the kit cannot
|
|
565
|
+
recover the sign-in. Files restored for the engine
|
|
489
566
|
have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
|
|
490
567
|
regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
|
|
491
568
|
dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
|
|
@@ -540,4 +617,4 @@ frozen (docs/runtime-kits.md §5).
|
|
|
540
617
|
|
|
541
618
|
Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
|
|
542
619
|
|
|
543
|
-
Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing; `kit.signedIn(member, 'claude-cli')` is true once Claude Code is signed in, and `'anthropic'` stays the API-billed provider a Claude Code login never satisfies. Before 0.7.0 this route reported provider `anthropic`; that value is deprecated, kept as `deprecatedProvider` until 0.
|
|
620
|
+
Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing; `kit.signedIn(member, 'claude-cli')` is true once Claude Code is signed in, and `'anthropic'` stays the API-billed provider a Claude Code login never satisfies. Before 0.7.0 this route reported provider `anthropic`; that value is deprecated, kept as `deprecatedProvider` until 0.9.0), with login kept in Claude Code under the kit’s isolated `HOME=<stateDir>/openclaw/home` and `CLAUDE_CONFIG_DIR=<stateDir>/openclaw/home/.claude`; sign in there with `claude auth login`, then call `signIn`. Activation live-tests the route; native sign-out is through Claude Code. `apiKey` is explicit API billing (“API key (billed per use)”, `offer: false`): `signIn` asks for the key through `paste`, then engine activation verifies it. The `setup-token` choice is listed but never offered: the pinned Gateway has no app-guided sign-in for it. The kit never imports Claude credentials or implements direct Claude.ai OAuth; [Anthropic’s terms](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use) apply.
|
package/SECURITY.md
CHANGED
|
@@ -29,9 +29,10 @@ retries after unlock. Opt-in dual wrapping opens through an owner-only host key
|
|
|
29
29
|
its protection is only as strong as that file, which must stay outside sealed-store backups.
|
|
30
30
|
Unlocked keyring-only snapshots upgrade atomically under the store's exclusive lock after verifying
|
|
31
31
|
the replacement decrypts to identical contents. A store the key cannot authenticate, or whose authentic
|
|
32
|
-
payload is not a snapshot, is never restored, overwritten or deleted: it
|
|
33
|
-
`auth-store.sealed
|
|
34
|
-
|
|
32
|
+
payload is not a snapshot, is never restored, renamed, overwritten or deleted: it stays byte-identical
|
|
33
|
+
at `auth-store.sealed`, blocking startup without plaintext fallback. No key rotation, reset or
|
|
34
|
+
new sign-in is attempted as recovery. See [credential sealing and recovery](README.md#credential-sealing-and-threat-model)
|
|
35
|
+
for the public error and host repair procedure. Other sealing failures still reject.
|
|
35
36
|
|
|
36
37
|
## Secrets at rest and in memory
|
|
37
38
|
|
package/dist/auth-status.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type AuthStatus = {
|
|
|
5
5
|
status?: unknown;
|
|
6
6
|
profiles?: {
|
|
7
7
|
status?: unknown;
|
|
8
|
+
expiresAt?: unknown;
|
|
8
9
|
}[];
|
|
9
10
|
usage?: unknown;
|
|
10
11
|
})[];
|
|
@@ -15,3 +16,11 @@ export type AuthStatus = {
|
|
|
15
16
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
16
17
|
* setup detection asks the CLI itself, without handing credentials to the kit. */
|
|
17
18
|
export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean): Promise<AuthStatus>;
|
|
19
|
+
/** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
|
|
20
|
+
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
21
|
+
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createAuthStatus(root: string, request: GatewayTransport['request']): {
|
|
24
|
+
read: (agentId: string, refresh?: boolean, native?: boolean, reuse?: boolean) => Promise<AuthStatus>;
|
|
25
|
+
invalidate: () => void;
|
|
26
|
+
};
|
package/dist/auth-status.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { signedInProviders, USABLE } from "./runs.js";
|
|
1
4
|
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
2
5
|
* setup detection asks the CLI itself, without handing credentials to the kit. */
|
|
3
6
|
export async function authStatus(request, agentId, refresh = false, native = false) {
|
|
4
7
|
const status = await request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 });
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
return native ? nativeStatus(request, agentId, status) : status;
|
|
9
|
+
}
|
|
10
|
+
async function nativeStatus(request, agentId, status) {
|
|
7
11
|
const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
|
|
8
12
|
const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
|
|
9
13
|
if (detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
|
|
@@ -11,3 +15,63 @@ export async function authStatus(request, agentId, refresh = false, native = fal
|
|
|
11
15
|
}
|
|
12
16
|
return { ...status, providers: rows };
|
|
13
17
|
}
|
|
18
|
+
/** Admission snapshots, never credentials: per kit/agent/native route, at most 30s and never past reported expiry.
|
|
19
|
+
* Witness only app-owned file metadata (absence included); changed/unreadable state and auth/lifecycle mutations recheck.
|
|
20
|
+
* Remote revocation still comes from the real run's signed-out error (a local status read cannot prove it either).
|
|
21
|
+
*/
|
|
22
|
+
export function createAuthStatus(root, request) {
|
|
23
|
+
const snapshots = new Map();
|
|
24
|
+
let generation = 0;
|
|
25
|
+
const invalidate = () => { generation++; snapshots.clear(); };
|
|
26
|
+
const witness = (agentId) => {
|
|
27
|
+
try {
|
|
28
|
+
return ['openclaw.json', `state/agents/${agentId}/agent/auth-profiles.json`,
|
|
29
|
+
'state/auth-profiles.json', 'state/agents/main/agent/auth-profiles.json',
|
|
30
|
+
'home/.claude/.credentials.json', 'home/.claude/.claude.json', 'home/.claude/settings.json',
|
|
31
|
+
'home/.claude.json', 'home/.codex/auth.json'].map(path => {
|
|
32
|
+
try {
|
|
33
|
+
const s = statSync(join(root, path), { bigint: true });
|
|
34
|
+
return `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return 'absent';
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}).join('|');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
} // unreadable witnesses never authorize reuse
|
|
46
|
+
};
|
|
47
|
+
const read = async (agentId, refresh = false, native = false, reuse = true) => {
|
|
48
|
+
const now = Date.now(), key = `${agentId}:${native}`, before = witness(agentId), epoch = generation;
|
|
49
|
+
for (const [key, snapshot] of snapshots)
|
|
50
|
+
if (snapshot.until <= now)
|
|
51
|
+
snapshots.delete(key);
|
|
52
|
+
const cached = snapshots.get(key);
|
|
53
|
+
if (reuse && !refresh && before !== undefined && cached?.witness === before && cached.until > now)
|
|
54
|
+
return cached.status;
|
|
55
|
+
snapshots.delete(key);
|
|
56
|
+
snapshots.delete(`${agentId}:false`);
|
|
57
|
+
const profiles = await authStatus(request, agentId, refresh);
|
|
58
|
+
const status = native ? await nativeStatus(request, agentId, profiles) : profiles;
|
|
59
|
+
if (before !== undefined && epoch === generation && witness(agentId) === before) {
|
|
60
|
+
for (const [route, answer] of (native ? [[false, profiles], [true, status]] : [[false, profiles]])) {
|
|
61
|
+
const usable = signedInProviders(answer);
|
|
62
|
+
if (!usable?.length || (route && !usable.includes('claude-cli')))
|
|
63
|
+
continue;
|
|
64
|
+
let until = now + 30_000;
|
|
65
|
+
for (const row of answer.providers ?? [])
|
|
66
|
+
if (typeof row !== 'string')
|
|
67
|
+
for (const p of row.profiles ?? [])
|
|
68
|
+
if (USABLE.has(String(p?.status)) && typeof p.expiresAt === 'number' && Number.isFinite(p.expiresAt))
|
|
69
|
+
until = Math.min(until, p.expiresAt);
|
|
70
|
+
if (until > Date.now())
|
|
71
|
+
snapshots.set(`${agentId}:${route}`, { status: answer, until, witness: before });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return status;
|
|
75
|
+
};
|
|
76
|
+
return { read, invalidate };
|
|
77
|
+
}
|
package/dist/auth-store.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
2
|
/** True for the regenerable cache paths the collector never seals; tests assert these are all that stay at rest. */
|
|
3
3
|
export declare function cached(name: string): boolean;
|
|
4
|
+
/** Opening failed; the original snapshot stays in place for repair and retry. */
|
|
5
|
+
export declare class AuthStoreUnreadableError extends Error {
|
|
6
|
+
readonly code = "auth-store-unreadable";
|
|
7
|
+
readonly reason: 'auth-failed' | 'invalid-snapshot';
|
|
8
|
+
constructor(reason: 'auth-failed' | 'invalid-snapshot');
|
|
9
|
+
}
|
|
4
10
|
export declare class AuthStore {
|
|
5
|
-
/** Set when an unreadable store was kept aside and the engine starts signed out; the kit clears it on sign-in. */
|
|
6
|
-
reset: boolean;
|
|
7
11
|
private active;
|
|
8
12
|
private owned;
|
|
9
13
|
private queue;
|
|
@@ -23,8 +27,6 @@ export declare class AuthStore {
|
|
|
23
27
|
private acquire;
|
|
24
28
|
private release;
|
|
25
29
|
private read;
|
|
26
|
-
/** Never overwrite or delete a store this kit cannot open: keep its bytes aside and start signed out. */
|
|
27
|
-
private setAside;
|
|
28
30
|
private collect;
|
|
29
31
|
/** Remove the live credential trees: `state` entirely (it is all credential state, including the
|
|
30
32
|
* runtime entries the collector skips), and in `home` exactly the sealed paths — regenerable caches
|
package/dist/auth-store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `home` — never the regenerable tool caches, transcripts and logs a signed-in home accumulates.
|
|
4
4
|
// Sealing those too once produced a snapshot past the runtime string limit and aborted boot.
|
|
5
5
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
|
|
6
|
-
import {
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
|
|
8
8
|
const encoder = new TextEncoder();
|
|
9
9
|
const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
|
|
@@ -88,9 +88,17 @@ function snapshot(text) {
|
|
|
88
88
|
throw new Error('invalid sealed credential parent');
|
|
89
89
|
return s;
|
|
90
90
|
}
|
|
91
|
+
/** Opening failed; the original snapshot stays in place for repair and retry. */
|
|
92
|
+
export class AuthStoreUnreadableError extends Error {
|
|
93
|
+
code = 'auth-store-unreadable';
|
|
94
|
+
reason;
|
|
95
|
+
constructor(reason) {
|
|
96
|
+
super("Saved sign-in could not be opened; auth-store.sealed is unchanged. Restore the original seal/key access or a working backup with its matching key, then retry.");
|
|
97
|
+
this.name = 'AuthStoreUnreadableError';
|
|
98
|
+
this.reason = reason;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
91
101
|
export class AuthStore {
|
|
92
|
-
/** Set when an unreadable store was kept aside and the engine starts signed out; the kit clears it on sign-in. */
|
|
93
|
-
reset = false;
|
|
94
102
|
active = false;
|
|
95
103
|
owned = false;
|
|
96
104
|
queue = Promise.resolve();
|
|
@@ -183,14 +191,14 @@ export class AuthStore {
|
|
|
183
191
|
catch (error) {
|
|
184
192
|
if (error?.code !== 'auth-failed')
|
|
185
193
|
throw error;
|
|
186
|
-
|
|
194
|
+
throw new AuthStoreUnreadableError('auth-failed');
|
|
187
195
|
}
|
|
188
196
|
let saved;
|
|
189
197
|
try {
|
|
190
198
|
saved = snapshot(text);
|
|
191
199
|
}
|
|
192
200
|
catch {
|
|
193
|
-
|
|
201
|
+
throw new AuthStoreUnreadableError('invalid-snapshot');
|
|
194
202
|
}
|
|
195
203
|
const upgraded = seal.upgrade?.(bytes);
|
|
196
204
|
if (upgraded) {
|
|
@@ -200,15 +208,6 @@ export class AuthStore {
|
|
|
200
208
|
}
|
|
201
209
|
return saved;
|
|
202
210
|
}
|
|
203
|
-
/** Never overwrite or delete a store this kit cannot open: keep its bytes aside and start signed out. */
|
|
204
|
-
setAside(why) {
|
|
205
|
-
const aside = `${this.file}.unreadable-${Date.now()}`;
|
|
206
|
-
renameSync(this.file, aside);
|
|
207
|
-
syncDir(this.o.root);
|
|
208
|
-
this.reset = true;
|
|
209
|
-
this.o.log?.(`sealed credential store ${why}; kept as ${basename(aside)}; sign in again`);
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
211
|
collect() {
|
|
213
212
|
const s = { v: 1, dirs: [], files: [] };
|
|
214
213
|
const root = realpathSync(this.o.root);
|
package/dist/engine-patches.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Internal artifact seam. Published patch semantics live only in engine/patches.json.
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { chmod, cp, open, readdir, rm } from 'node:fs/promises';
|
|
@@ -180,22 +181,27 @@ async function removeTemp(dir) {
|
|
|
180
181
|
await walk(dir);
|
|
181
182
|
await rm(dir, { recursive: true, force: true });
|
|
182
183
|
}
|
|
184
|
+
const flushFs = (dir) => new Promise(resolve => {
|
|
185
|
+
// One filesystem-wide syncfs through `sync -f` (~2 s vs ~110 s of per-file fsyncs on the reference btrfs host);
|
|
186
|
+
// durability rule: docs/runtime-kits.md 5.16.
|
|
187
|
+
if (process.platform !== 'linux')
|
|
188
|
+
return resolve();
|
|
189
|
+
const child = spawn('sync', ['-f', dir], { stdio: 'ignore' });
|
|
190
|
+
child.once('error', () => resolve());
|
|
191
|
+
child.once('exit', () => resolve());
|
|
192
|
+
});
|
|
193
|
+
// Freeze makes the tree read-only and returns its entries as the manifest; durability rule: docs/runtime-kits.md 5.16.
|
|
183
194
|
async function freeze(dir) {
|
|
184
|
-
|
|
185
|
-
//
|
|
195
|
+
// Bound I/O while yielding the host event loop: chmod only, in batches. Links are left untouched (and keep
|
|
196
|
+
// their recorded mode); every file becomes 0444 and every directory 0555, exactly what the manifest records.
|
|
197
|
+
const entries = tree(dir).map(entry => entry.kind === 'link' ? entry : { ...entry, mode: entry.kind === 'dir' ? 0o555 : 0o444 });
|
|
186
198
|
const paths = [...entries.filter(e => e.kind === 'file'), ...entries.filter(e => e.kind === 'dir').reverse()];
|
|
187
199
|
for (let i = 0; i < paths.length; i += 16)
|
|
188
200
|
await Promise.all(paths.slice(i, i + 16).map(async (entry) => {
|
|
189
|
-
|
|
190
|
-
await chmod(path, entry.kind === 'dir' ? 0o555 : 0o444);
|
|
191
|
-
const fd = await open(path, 'r');
|
|
192
|
-
try {
|
|
193
|
-
await fd.sync();
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
await fd.close();
|
|
197
|
-
}
|
|
201
|
+
await chmod(join(dir, entry.path), entry.mode);
|
|
198
202
|
}));
|
|
203
|
+
await flushFs(dir);
|
|
204
|
+
return entries;
|
|
199
205
|
}
|
|
200
206
|
export function engineSetName(set) { return `${sha256(set.upstream.integrity).slice(0, 16)}-${set.id}`; }
|
|
201
207
|
export async function prepareEngineSet(engineDir, set, install, installMatches) {
|
|
@@ -257,8 +263,8 @@ export async function prepareEngineSet(engineDir, set, install, installMatches)
|
|
|
257
263
|
atomic(path, after);
|
|
258
264
|
}
|
|
259
265
|
metadata(tmp, set);
|
|
260
|
-
await freeze(tmp);
|
|
261
|
-
const bytes = JSON.stringify(
|
|
266
|
+
const entries = await freeze(tmp);
|
|
267
|
+
const bytes = JSON.stringify(entries);
|
|
262
268
|
// Root alone is writable while publishing metadata, then made read-only too.
|
|
263
269
|
await chmod(tmp, 0o700);
|
|
264
270
|
atomic(join(tmp, '.byokit-tree'), bytes);
|
package/dist/engine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from 'node:net';
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
-
import { AuthStore } from "./auth-store.js";
|
|
8
|
+
import { AuthStore, AuthStoreUnreadableError } from "./auth-store.js";
|
|
9
9
|
import { EngineAlreadyRunningError, pidAlive, StartedProcesses } from "./engine-status.js";
|
|
10
10
|
import { EnginePatchError, atomic, prepareEngineSet, processStartTime, readPatchSet, verifyEngineSet } from "./engine-patches.js";
|
|
11
11
|
import { ENGINE_VERSION } from "./constants.js";
|
|
@@ -102,6 +102,8 @@ export class Engine {
|
|
|
102
102
|
this.patchSet = null;
|
|
103
103
|
this.state('failed', 'engine-patch');
|
|
104
104
|
}
|
|
105
|
+
if (error instanceof AuthStoreUnreadableError)
|
|
106
|
+
this.state('failed', 'auth-store-unreadable');
|
|
105
107
|
throw error;
|
|
106
108
|
});
|
|
107
109
|
this.prepared = pending;
|
|
@@ -293,6 +295,8 @@ export class Engine {
|
|
|
293
295
|
catch (error) {
|
|
294
296
|
if (this.locked(error))
|
|
295
297
|
return undefined;
|
|
298
|
+
if (error instanceof AuthStoreUnreadableError)
|
|
299
|
+
this.state('failed', 'auth-store-unreadable');
|
|
296
300
|
throw error;
|
|
297
301
|
}
|
|
298
302
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
2
|
+
export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export type { GatewayMethods } from './generated/methods.ts';
|
|
4
4
|
export type { JsonValue } from './output.ts';
|
|
5
5
|
export type { RouteFacts, RouteView } from './routes.ts';
|
|
6
6
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
7
7
|
export type { AddKeyResult } from './keys.ts';
|
|
8
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
9
|
+
export { AuthStoreUnreadableError } from './auth-store.ts';
|
|
9
10
|
export { EngineAlreadyRunningError } from './engine-status.ts';
|
|
10
11
|
export { readAgentDayUsage } from './day-usage.ts';
|
|
11
12
|
export type { AgentDayUsage, DayUsageClient, EngineStartedCharge, EngineStartedKind } from './day-usage.ts';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from "./constants.js";
|
|
4
4
|
export { OpenClawKit } from "./kit.js";
|
|
5
5
|
export { stateWords, toAccountView, words } from "./words.js";
|
|
6
|
+
export { AuthStoreUnreadableError } from "./auth-store.js";
|
|
6
7
|
export { EngineAlreadyRunningError } from "./engine-status.js";
|
|
7
8
|
export { readAgentDayUsage } from "./day-usage.js";
|
|
8
9
|
export { readAgentUsage, agentUsageOf } from "./usage.js";
|
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
2
|
import { type AddKeyResult } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
|
-
import type { Approval, CallOptions, Decision, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, Member, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Approval, CallOptions, Decision, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, RunEnd, RunEvent, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -55,6 +55,7 @@ export declare class OpenClawKit {
|
|
|
55
55
|
private members?;
|
|
56
56
|
private readonly bridge;
|
|
57
57
|
private readonly keys;
|
|
58
|
+
private readonly admission;
|
|
58
59
|
private readonly approvalsCtl;
|
|
59
60
|
private listeners;
|
|
60
61
|
private off;
|
|
@@ -99,6 +100,7 @@ export declare class OpenClawKit {
|
|
|
99
100
|
}>;
|
|
100
101
|
private signInCtx;
|
|
101
102
|
routes(): RouteView[];
|
|
103
|
+
providerStatus(member: Member): Promise<string[] | undefined>;
|
|
102
104
|
providers(member: Member): Promise<string[]>;
|
|
103
105
|
signedIn(member: Member, provider: string): Promise<boolean>;
|
|
104
106
|
signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
|
|
@@ -140,6 +142,10 @@ export declare class OpenClawKit {
|
|
|
140
142
|
memoryLimited(member: Member): boolean;
|
|
141
143
|
getConfigKey(key: string): unknown;
|
|
142
144
|
setConfigKey(key: string, value: unknown): unknown;
|
|
145
|
+
learning(): LearningCapture;
|
|
146
|
+
setLearning(mode: LearningMode | 'default'): LearningCapture;
|
|
147
|
+
restoreLearning(captured: LearningCapture): LearningCapture;
|
|
148
|
+
private assertLearningWritable;
|
|
143
149
|
doctorContext(): {
|
|
144
150
|
entry: string;
|
|
145
151
|
env: Record<string, string>;
|
package/dist/kit.js
CHANGED
|
@@ -11,15 +11,18 @@ import { gatewayTransport } from "./transport.js";
|
|
|
11
11
|
import { createMembers } from "./members.js";
|
|
12
12
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
|
|
13
13
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
|
|
14
|
-
import { createRuns } from "./runs.js";
|
|
14
|
+
import { createRuns, signedInProviders } from "./runs.js";
|
|
15
|
+
import { createAuthStatus } from "./auth-status.js";
|
|
15
16
|
import { outputSchema } from "./output.js";
|
|
16
17
|
import { createKeys } from "./keys.js";
|
|
17
18
|
import { routes as routeTable } from "./routes.js";
|
|
18
|
-
import {
|
|
19
|
+
import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
|
|
19
20
|
import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
|
|
20
21
|
import { createBrowserHost } from "./browser/host.js";
|
|
21
22
|
import { fileSignInStore } from "./browser/store.js";
|
|
22
23
|
import { launchBroker } from "./browser/broker.js";
|
|
24
|
+
/** The engine's learning-mode config key (5.6 "Engine learning state"). */
|
|
25
|
+
const LEARNING_KEY = 'skills.workshop.autonomous.mode';
|
|
23
26
|
const operatorMethods = new Set([
|
|
24
27
|
'agent',
|
|
25
28
|
'agent.identity.get',
|
|
@@ -428,6 +431,7 @@ export class OpenClawKit {
|
|
|
428
431
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
429
432
|
bridge;
|
|
430
433
|
keys;
|
|
434
|
+
admission;
|
|
431
435
|
approvalsCtl;
|
|
432
436
|
listeners = new Set();
|
|
433
437
|
off = [];
|
|
@@ -644,6 +648,7 @@ export class OpenClawKit {
|
|
|
644
648
|
...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
|
|
645
649
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
646
650
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
651
|
+
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
|
|
647
652
|
this.keys = createKeys({ root: this.engine.root,
|
|
648
653
|
restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
|
|
649
654
|
request: (method, params, options) => this.request()(method, params, options),
|
|
@@ -684,6 +689,8 @@ export class OpenClawKit {
|
|
|
684
689
|
slot.bridge = this.bridge;
|
|
685
690
|
}
|
|
686
691
|
setState(s) {
|
|
692
|
+
if (s.phase !== 'ready')
|
|
693
|
+
this.admission?.invalidate();
|
|
687
694
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
688
695
|
this.o.onState?.(this.current);
|
|
689
696
|
}
|
|
@@ -736,7 +743,7 @@ export class OpenClawKit {
|
|
|
736
743
|
await this.bridge.start();
|
|
737
744
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
738
745
|
this.failures = 0;
|
|
739
|
-
this.setState(
|
|
746
|
+
this.setState({ phase: 'ready' });
|
|
740
747
|
if (this.o.browser)
|
|
741
748
|
await this.checkBrowserTools();
|
|
742
749
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
@@ -753,6 +760,10 @@ export class OpenClawKit {
|
|
|
753
760
|
this.setState({ phase: 'failed', why: 'engine-already-running' });
|
|
754
761
|
throw error;
|
|
755
762
|
}
|
|
763
|
+
if (error instanceof Error && 'code' in error && error.code === 'auth-store-unreadable') {
|
|
764
|
+
this.setState({ phase: 'failed', why: 'auth-store-unreadable' });
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
756
767
|
if (patchFailure) {
|
|
757
768
|
this.setState({ phase: 'failed', why: 'engine-patch' });
|
|
758
769
|
throw error;
|
|
@@ -765,6 +776,7 @@ export class OpenClawKit {
|
|
|
765
776
|
}
|
|
766
777
|
}
|
|
767
778
|
async disconnect() {
|
|
779
|
+
this.admission.invalidate();
|
|
768
780
|
this.off.splice(0).forEach((fn) => fn());
|
|
769
781
|
const transport = this.transport;
|
|
770
782
|
this.transport = undefined;
|
|
@@ -812,7 +824,14 @@ export class OpenClawKit {
|
|
|
812
824
|
request() {
|
|
813
825
|
if (!this.transport || this.current.phase !== 'ready')
|
|
814
826
|
throw new Error('gateway not ready');
|
|
815
|
-
|
|
827
|
+
const transport = this.transport;
|
|
828
|
+
return (method, params, options) => {
|
|
829
|
+
const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
|
|
830
|
+
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
831
|
+
return transport.request(method, params, options);
|
|
832
|
+
this.admission.invalidate();
|
|
833
|
+
return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => this.admission.invalidate());
|
|
834
|
+
};
|
|
816
835
|
}
|
|
817
836
|
call(method, params, o) {
|
|
818
837
|
if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
|
|
@@ -869,27 +888,26 @@ export class OpenClawKit {
|
|
|
869
888
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
870
889
|
signInCtx() {
|
|
871
890
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
891
|
+
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
892
|
+
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
872
893
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
873
894
|
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
|
|
874
895
|
}
|
|
875
896
|
routes() {
|
|
876
897
|
return routeTable();
|
|
877
898
|
}
|
|
899
|
+
async providerStatus(member) {
|
|
900
|
+
const { agentId } = await this.ensureMember(member);
|
|
901
|
+
return signedInProviders(await this.admission.read(agentId, false, true, false));
|
|
902
|
+
}
|
|
878
903
|
providers(member) {
|
|
879
|
-
return
|
|
904
|
+
return this.providerStatus(member).then((names) => names ?? []);
|
|
880
905
|
}
|
|
881
906
|
signedIn(member, provider) {
|
|
882
|
-
return
|
|
907
|
+
return this.providers(member).then((names) => names.includes(provider)).catch(() => false);
|
|
883
908
|
}
|
|
884
909
|
signIn(member, o, on) {
|
|
885
|
-
return startSignIn(this.signInCtx(), member, o,
|
|
886
|
-
if (view.state === 'done' && this.engine.authStore.reset) {
|
|
887
|
-
this.engine.authStore.reset = false;
|
|
888
|
-
if (this.current.phase === 'ready')
|
|
889
|
-
this.setState({ phase: 'ready' });
|
|
890
|
-
}
|
|
891
|
-
on(view);
|
|
892
|
-
});
|
|
910
|
+
return startSignIn(this.signInCtx(), member, o, on);
|
|
893
911
|
}
|
|
894
912
|
signOut(member, provider) {
|
|
895
913
|
return engineSignOut(this.signInCtx(), member, provider);
|
|
@@ -910,6 +928,8 @@ export class OpenClawKit {
|
|
|
910
928
|
runs() {
|
|
911
929
|
return createRuns({
|
|
912
930
|
request: (method, params, o) => this.request()(method, params, o),
|
|
931
|
+
authStatus: this.admission.read,
|
|
932
|
+
invalidateAuth: this.admission.invalidate,
|
|
913
933
|
onEvent: (fn) => {
|
|
914
934
|
this.listeners.add(fn);
|
|
915
935
|
return () => {
|
|
@@ -967,7 +987,7 @@ export class OpenClawKit {
|
|
|
967
987
|
steer(sessionKey, text, o) {
|
|
968
988
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
969
989
|
}
|
|
970
|
-
abort(sessionKey, o) {
|
|
990
|
+
async abort(sessionKey, o) {
|
|
971
991
|
return this.runs().abort(this.runKey(sessionKey, o));
|
|
972
992
|
}
|
|
973
993
|
// approvals (5.9)
|
|
@@ -1024,5 +1044,38 @@ export class OpenClawKit {
|
|
|
1024
1044
|
atomic(path, next);
|
|
1025
1045
|
return previous;
|
|
1026
1046
|
}
|
|
1047
|
+
// The engine's learning switch, named: a typed veneer over the narrow pair above, file-backed and boot-free
|
|
1048
|
+
// (5.6 "Engine learning state"). The engine itself falls back silently on a stored value outside its enum;
|
|
1049
|
+
// these refuse, so a typo or a corrupted file surfaces instead of quietly flipping learning.
|
|
1050
|
+
learning() {
|
|
1051
|
+
const value = this.getConfigKey(LEARNING_KEY);
|
|
1052
|
+
if (value === undefined)
|
|
1053
|
+
return { present: false };
|
|
1054
|
+
if (value !== 'off' && value !== 'propose' && value !== 'auto')
|
|
1055
|
+
throw new Error(`learning: stored ${LEARNING_KEY} is ${JSON.stringify(value)}, not one of off|propose|auto`);
|
|
1056
|
+
return { present: true, mode: value };
|
|
1057
|
+
}
|
|
1058
|
+
setLearning(mode) {
|
|
1059
|
+
if (mode !== 'off' && mode !== 'propose' && mode !== 'auto' && mode !== 'default')
|
|
1060
|
+
throw new Error(`setLearning: unknown learning mode ${JSON.stringify(mode)}, expected off|propose|auto|default`);
|
|
1061
|
+
this.assertLearningWritable('setLearning');
|
|
1062
|
+
const previous = this.learning();
|
|
1063
|
+
this.setConfigKey(LEARNING_KEY, mode === 'default' ? undefined : mode);
|
|
1064
|
+
return previous;
|
|
1065
|
+
}
|
|
1066
|
+
restoreLearning(captured) {
|
|
1067
|
+
if (captured.present && captured.mode !== 'off' && captured.mode !== 'propose' && captured.mode !== 'auto')
|
|
1068
|
+
throw new Error(`restoreLearning: unknown learning mode ${JSON.stringify(captured.mode)}, expected off|propose|auto`);
|
|
1069
|
+
this.assertLearningWritable('restoreLearning');
|
|
1070
|
+
this.setConfigKey(LEARNING_KEY, captured.present ? captured.mode : undefined);
|
|
1071
|
+
const read = this.learning();
|
|
1072
|
+
if (read.present !== captured.present || (read.present && captured.present && read.mode !== captured.mode))
|
|
1073
|
+
throw new Error(`learning restore readback mismatch: captured ${JSON.stringify(captured)}, read ${JSON.stringify(read)}`);
|
|
1074
|
+
return read;
|
|
1075
|
+
}
|
|
1076
|
+
assertLearningWritable(op) {
|
|
1077
|
+
if (this.o.config?.skills !== undefined)
|
|
1078
|
+
throw new Error(`${op}: KitOptions.config also passes skills; that option wins at every boot (5.6)`);
|
|
1079
|
+
}
|
|
1027
1080
|
doctorContext() { return this.engine.doctorContext(); }
|
|
1028
1081
|
}
|
package/dist/link.js
CHANGED
|
@@ -10,9 +10,7 @@ import { reach } from '@byokit/discover';
|
|
|
10
10
|
import { ENGINE_VERSION } from "./constants.js";
|
|
11
11
|
import { b64urlDecode, b64urlEncode, sealNotice } from "./notices.js";
|
|
12
12
|
import { routeFor } from "./routes.js";
|
|
13
|
-
import { authStatus } from "./auth-status.js";
|
|
14
13
|
import { outputSchema } from "./output.js";
|
|
15
|
-
import { signedInProviders } from "./runs.js";
|
|
16
14
|
import { stateWords, words } from "./words.js";
|
|
17
15
|
const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
|
|
18
16
|
'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
|
|
@@ -228,20 +226,6 @@ export function openclawLink(kit, o) {
|
|
|
228
226
|
return null;
|
|
229
227
|
};
|
|
230
228
|
const endReason = (error) => error instanceof PublicLinkError ? error.message.slice(0, 200) : 'failed';
|
|
231
|
-
const signIns = async (member) => {
|
|
232
|
-
try {
|
|
233
|
-
const { agentId } = await kit.ensureMember(member);
|
|
234
|
-
const status = await authStatus((method, params, options) => {
|
|
235
|
-
if (method === 'models.authStatus')
|
|
236
|
-
return kit.call(method, params, options);
|
|
237
|
-
return kit.call('openclaw.setup.detect', params, options);
|
|
238
|
-
}, agentId, false, true);
|
|
239
|
-
return signedInProviders(status);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return undefined;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
229
|
const handle = async (req, grant) => {
|
|
246
230
|
const member = memberOf(grant);
|
|
247
231
|
if (grant.role === 'view' && !VIEW_OPS.has(req.op))
|
|
@@ -314,7 +298,7 @@ export function openclawLink(kit, o) {
|
|
|
314
298
|
case 'oc.state': {
|
|
315
299
|
// signedIn: the providers the device member is usably signed in to (an expired or unfinished sign-in is not),
|
|
316
300
|
// only while the engine can say: absent is unknown, never "none".
|
|
317
|
-
const signedIn = kit.state.phase === 'ready' ? await
|
|
301
|
+
const signedIn = kit.state.phase === 'ready' ? await kit.providerStatus(member).catch(() => undefined) : undefined;
|
|
318
302
|
return { state: kit.state, words: stateWords(kit.state), version: KIT_VERSION, engine: ENGINE_VERSION,
|
|
319
303
|
...(signedIn ? { signedIn } : {}) };
|
|
320
304
|
}
|
package/dist/members.js
CHANGED
|
@@ -2,8 +2,13 @@ import { join } from 'node:path';
|
|
|
2
2
|
export const MEMBER_ID = /^[a-z][a-z0-9-]{0,31}$/;
|
|
3
3
|
export const KEY_PREFIX = 'byokit-key-';
|
|
4
4
|
export const keyAgentId = (member) => `${KEY_PREFIX}${member}`;
|
|
5
|
+
// The engine answers agents.create before its roster hot-reload applies (up to ~113 ms observed), so a turn sent
|
|
6
|
+
// at once fails as 'unknown agent id'. ensure resolves only once agents.list shows the member.
|
|
7
|
+
const VISIBLE_TIMEOUT_MS = 5_000;
|
|
8
|
+
const VISIBLE_POLL_MS = 25;
|
|
5
9
|
export function createMembers(ctx) {
|
|
6
10
|
const cache = new Map();
|
|
11
|
+
const listed = (member) => ctx.request('agents.list').then((list) => list.agents.some((agent) => agent.id === member));
|
|
7
12
|
return { ensure(member) {
|
|
8
13
|
if (!MEMBER_ID.test(member) || member.startsWith(KEY_PREFIX))
|
|
9
14
|
return Promise.reject(new Error(`invalid member id: ${member}`));
|
|
@@ -12,9 +17,15 @@ export function createMembers(ctx) {
|
|
|
12
17
|
return existing;
|
|
13
18
|
const workspace = join(ctx.root, 'workspaces', member);
|
|
14
19
|
const result = (async () => {
|
|
15
|
-
|
|
16
|
-
if (!list.agents.some((agent) => agent.id === member))
|
|
20
|
+
if (!await listed(member)) {
|
|
17
21
|
await ctx.request('agents.create', { name: member, workspace });
|
|
22
|
+
const deadline = Date.now() + VISIBLE_TIMEOUT_MS;
|
|
23
|
+
while (!await listed(member)) {
|
|
24
|
+
if (Date.now() >= deadline)
|
|
25
|
+
throw new Error(`agent ${member} is not listed by agents.list after agents.create`);
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, VISIBLE_POLL_MS));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
18
29
|
return { agentId: member, workspace };
|
|
19
30
|
})();
|
|
20
31
|
cache.set(member, result);
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Bridge } from './bridge.ts';
|
|
2
|
+
import { authStatus } from './auth-status.ts';
|
|
2
3
|
import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
|
|
3
4
|
import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
|
|
5
|
+
export declare const USABLE: Set<string>;
|
|
4
6
|
/**
|
|
5
7
|
* The providers a `models.authStatus` answer shows usable (the rule a picked account is checked by), lowercased and
|
|
6
8
|
* unique; undefined while the engine has no prepared status (unknown, not none).
|
|
@@ -8,6 +10,8 @@ import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './type
|
|
|
8
10
|
export declare function signedInProviders(status: unknown): string[] | undefined;
|
|
9
11
|
export declare function createRuns(ctx: {
|
|
10
12
|
request: GatewayTransport['request'];
|
|
13
|
+
authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
|
|
14
|
+
invalidateAuth?: () => void;
|
|
11
15
|
onEvent: GatewayTransport['onEvent'];
|
|
12
16
|
ensure(member: Member): Promise<{
|
|
13
17
|
agentId: string;
|
package/dist/runs.js
CHANGED
|
@@ -11,7 +11,7 @@ const WAIT_CLIENT_MS = 3_610_000;
|
|
|
11
11
|
// The `agent` request's final frame lands with the run's end; a run already over waits no longer than this for it.
|
|
12
12
|
const FINAL_GRACE_MS = 5_000;
|
|
13
13
|
const STATUS_MS = 5_000;
|
|
14
|
-
const USABLE = new Set(['ok', 'expiring', 'static']);
|
|
14
|
+
export const USABLE = new Set(['ok', 'expiring', 'static']);
|
|
15
15
|
const usable = (row) => typeof row === 'string'
|
|
16
16
|
|| (Array.isArray(row.profiles) && row.profiles.length > 0 ? row.profiles.some((p) => USABLE.has(String(p?.status)))
|
|
17
17
|
: row.status === undefined || USABLE.has(String(row.status)));
|
|
@@ -80,6 +80,12 @@ export function createRuns(ctx) {
|
|
|
80
80
|
return undefined;
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
|
+
const failure = (message) => {
|
|
84
|
+
const classified = classify(message);
|
|
85
|
+
if (classified.kind === 'signed-out')
|
|
86
|
+
ctx.invalidateAuth?.();
|
|
87
|
+
return { ok: false, ...classified, message };
|
|
88
|
+
};
|
|
83
89
|
const run = async (spec, on, keyAgent, preparedOutput) => {
|
|
84
90
|
// Member boundary first: a member never speaks in another member's session, refused before any request.
|
|
85
91
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`))
|
|
@@ -112,7 +118,10 @@ export function createRuns(ctx) {
|
|
|
112
118
|
if (p.stream === 'lifecycle' && p.data?.phase === 'end'
|
|
113
119
|
&& p.data.aborted === true)
|
|
114
120
|
abortedByEngine = true;
|
|
115
|
-
if (p.stream === '
|
|
121
|
+
if (p.stream === 'thinking' && count(p.data?.progressTokens)) {
|
|
122
|
+
on?.({ type: 'thinking', tokens: p.data.progressTokens });
|
|
123
|
+
}
|
|
124
|
+
else if (p.stream === 'assistant' && typeof p.data?.text === 'string') {
|
|
116
125
|
last = p.data.text;
|
|
117
126
|
on?.({ type: 'text', text: p.data.text });
|
|
118
127
|
}
|
|
@@ -135,7 +144,8 @@ export function createRuns(ctx) {
|
|
|
135
144
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
136
145
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
|
137
146
|
// the pinned engine (never another provider or model), so this check plus the override is the guarantee.
|
|
138
|
-
const status = (refresh) => authStatus
|
|
147
|
+
const status = (refresh) => ctx.authStatus ? ctx.authStatus(agentId, refresh, picked.provider === 'claude-cli')
|
|
148
|
+
: authStatus(ctx.request, agentId, refresh, picked.provider === 'claude-cli');
|
|
139
149
|
let auth = await status(false);
|
|
140
150
|
if (auth.unavailable)
|
|
141
151
|
auth = await status(true); // no prepared snapshot yet: build it once
|
|
@@ -151,8 +161,12 @@ export function createRuns(ctx) {
|
|
|
151
161
|
const ack = new Promise((resolve) => { accepted = resolve; });
|
|
152
162
|
// The run id is taken as the accepted frame lands, so no event read after it is missed.
|
|
153
163
|
const onAccepted = (payload) => {
|
|
154
|
-
if (isRecord(payload) && typeof payload.runId === 'string')
|
|
164
|
+
if (isRecord(payload) && typeof payload.runId === 'string') {
|
|
165
|
+
const first = !runId;
|
|
155
166
|
runId = payload.runId;
|
|
167
|
+
if (first && payload.status === 'accepted')
|
|
168
|
+
on?.({ type: 'started' });
|
|
169
|
+
}
|
|
156
170
|
accepted(payload);
|
|
157
171
|
};
|
|
158
172
|
const final = ctx.request('agent', {
|
|
@@ -199,11 +213,11 @@ export function createRuns(ctx) {
|
|
|
199
213
|
return { ok: false, aborted: true };
|
|
200
214
|
const message = typeof result.error === 'string' ? result.error
|
|
201
215
|
: typeof result.message === 'string' ? result.message : '';
|
|
202
|
-
return
|
|
216
|
+
return failure(message);
|
|
203
217
|
}
|
|
204
218
|
catch (error) {
|
|
205
219
|
const message = error instanceof Error ? error.message : String(error);
|
|
206
|
-
return
|
|
220
|
+
return failure(message);
|
|
207
221
|
}
|
|
208
222
|
finally {
|
|
209
223
|
ended = true;
|
|
@@ -214,6 +228,6 @@ export function createRuns(ctx) {
|
|
|
214
228
|
return {
|
|
215
229
|
run,
|
|
216
230
|
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: k, message: t }),
|
|
217
|
-
abort: (k) => ctx.request('chat.abort', { sessionKey: k }),
|
|
231
|
+
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: k }); },
|
|
218
232
|
};
|
|
219
233
|
}
|
package/dist/signin.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { authStatus } from './auth-status.ts';
|
|
1
2
|
import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
|
|
2
3
|
export type SignInCtx = {
|
|
3
4
|
request: GatewayTransport['request'];
|
|
5
|
+
authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
|
|
4
6
|
ensure(member: Member): Promise<{
|
|
5
7
|
agentId: string;
|
|
6
8
|
}>;
|
package/dist/signin.js
CHANGED
|
@@ -430,7 +430,8 @@ export function signIn(ctx, member, o, on) {
|
|
|
430
430
|
/** Usable provider profiles plus native Claude Code readiness reported by the engine. */
|
|
431
431
|
export async function providers(ctx, member, refresh) {
|
|
432
432
|
const { agentId } = await ctx.ensure(member);
|
|
433
|
-
return signedInProviders(await
|
|
433
|
+
return signedInProviders(await (ctx.authStatus ? ctx.authStatus(agentId, refresh ?? false, true)
|
|
434
|
+
: authStatus(ctx.request, agentId, refresh, true))) ?? [];
|
|
434
435
|
}
|
|
435
436
|
export async function signOut(ctx, member, provider) {
|
|
436
437
|
const { agentId } = await ctx.ensure(member);
|
package/dist/testing/contract.js
CHANGED
|
@@ -65,6 +65,19 @@ export function openclawContract(make, o) {
|
|
|
65
65
|
await kit.stop();
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
|
+
test('contract: a turn sent right after ensureMember is not an unknown agent', async () => {
|
|
69
|
+
const { kit } = await make();
|
|
70
|
+
try {
|
|
71
|
+
// The roster applies a beat after agents.create answers (engine hot-reload); ensureMember must wait
|
|
72
|
+
// until agents.list shows the member, so the first turn is not rejected as 'unknown agent id'.
|
|
73
|
+
await kit.ensureMember('m1');
|
|
74
|
+
const end = await kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:6', message: 'hello fresh member' });
|
|
75
|
+
assert.ok(end.ok, JSON.stringify(end));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await kit.stop();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
68
81
|
test('contract: a member cannot run in another member\'s session', async () => {
|
|
69
82
|
const { kit } = await make();
|
|
70
83
|
try {
|
|
@@ -94,7 +94,7 @@ function bridgeRequest(path, message) {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
export function fakeGateway(script) {
|
|
97
|
+
export function fakeGateway(script, o) {
|
|
98
98
|
const calls = [];
|
|
99
99
|
const failures = new Map();
|
|
100
100
|
const handlers = new Map();
|
|
@@ -212,8 +212,15 @@ export function fakeGateway(script) {
|
|
|
212
212
|
agents: [...agents.values()].map((agent) => ({ id: agent.id })),
|
|
213
213
|
}),
|
|
214
214
|
'agents.create': (p) => {
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
const id = String(p.name);
|
|
216
|
+
const workspace = p.workspace ? String(p.workspace) : undefined;
|
|
217
|
+
// With rosterApplyMs the fake mirrors the pinned engine: create answers before the roster hot-reload
|
|
218
|
+
// makes the agent visible to agents.list (Crewhouse saw ~113 ms).
|
|
219
|
+
if (o?.rosterApplyMs)
|
|
220
|
+
setTimeout(() => ensureAgent(id, workspace), o.rosterApplyMs);
|
|
221
|
+
else
|
|
222
|
+
ensureAgent(id, workspace);
|
|
223
|
+
return { id };
|
|
217
224
|
},
|
|
218
225
|
'models.authStatus': (p) => ({
|
|
219
226
|
providers: (agents.get(String(p?.agentId ?? 'main'))?.providers ?? []).map((provider) => ({ provider })),
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ import type { OutputSchema } from './output.ts';
|
|
|
2
2
|
import type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
|
|
3
3
|
export type { OutputSchema, SchemaOutput } from './output.ts';
|
|
4
4
|
export type Member = string;
|
|
5
|
+
/** The engine's learning mode: config key `skills.workshop.autonomous.mode` (5.6). */
|
|
6
|
+
export type LearningMode = 'off' | 'propose' | 'auto';
|
|
7
|
+
/** Exactly what `learning()` saw: absence is a value, not "probably default" (5.6). */
|
|
8
|
+
export type LearningCapture = {
|
|
9
|
+
present: false;
|
|
10
|
+
} | {
|
|
11
|
+
present: true;
|
|
12
|
+
mode: LearningMode;
|
|
13
|
+
};
|
|
5
14
|
export interface ToolSpec {
|
|
6
15
|
name: string;
|
|
7
16
|
description: string;
|
|
@@ -47,6 +56,11 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
|
|
|
47
56
|
register?: boolean;
|
|
48
57
|
}
|
|
49
58
|
export type RunEvent = {
|
|
59
|
+
type: 'started';
|
|
60
|
+
} | {
|
|
61
|
+
type: 'thinking';
|
|
62
|
+
tokens: number;
|
|
63
|
+
} | {
|
|
50
64
|
type: 'text';
|
|
51
65
|
text: string;
|
|
52
66
|
} | {
|
|
@@ -127,7 +141,7 @@ export type Decision = {
|
|
|
127
141
|
};
|
|
128
142
|
export type KitState = {
|
|
129
143
|
phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
|
|
130
|
-
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset';
|
|
144
|
+
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset' | 'auth-store-unreadable';
|
|
131
145
|
retryAt?: number;
|
|
132
146
|
patchSet?: string | null;
|
|
133
147
|
};
|
|
@@ -160,7 +174,7 @@ export type Route = {
|
|
|
160
174
|
via: 'browser' | 'code';
|
|
161
175
|
};
|
|
162
176
|
/** @deprecated This route's `provider` before 0.7.0 (`anthropic` on `anthropic-cli`); use `provider` (`claude-cli`).
|
|
163
|
-
* Discovery data only, never a sign-in or `signedIn` id. Removed in 0.
|
|
177
|
+
* Discovery data only, never a sign-in or `signedIn` id. Removed in 0.9.0, when that route's id becomes
|
|
164
178
|
* `claude-cli:cli:anthropic-cli`. */
|
|
165
179
|
deprecatedProvider?: string;
|
|
166
180
|
billingFrom?: 'source' | 'host';
|
package/dist/words.js
CHANGED
|
@@ -21,6 +21,8 @@ export function stateWords(s) {
|
|
|
21
21
|
// 'stopped' is never on screen (4.3 covers what a person can see), so 5.14 has no row for it.
|
|
22
22
|
if (s.phase === 'failed' && s.why === 'engine-already-running')
|
|
23
23
|
return words('engine.alreadyRunning');
|
|
24
|
+
if (s.why === 'auth-store-unreadable')
|
|
25
|
+
return words('engine.authStoreUnreadable');
|
|
24
26
|
if (s.why === 'sign-in-reset')
|
|
25
27
|
return words('engine.signInAgain');
|
|
26
28
|
return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
|
package/dist/words.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"engine.starting": "Starting up…",
|
|
4
4
|
"engine.repairing": "Fixing a small problem with the setup. This takes a moment.",
|
|
5
5
|
"engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
|
|
6
|
+
"engine.authStoreUnreadable": "Your saved sign-in couldn't be opened. It is unchanged. Restore its original key or a working backup, then retry.",
|
|
6
7
|
"engine.signInAgain": "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again.",
|
|
7
8
|
"engine.ready": "Ready.",
|
|
8
9
|
"engine.restarting": "Something stopped. Starting it again by itself.",
|
package/engine/patches.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"v": 1,
|
|
3
|
-
"id": "
|
|
3
|
+
"id": "ee8913bf172b63b2",
|
|
4
4
|
"upstream": {
|
|
5
5
|
"name": "openclaw",
|
|
6
6
|
"version": "2026.8.1",
|
|
@@ -54,6 +54,17 @@
|
|
|
54
54
|
"replace": "// BYOKit: caller-owned task prefixes never acquire automatic restart recovery ownership.\nconst byokitAppOwnedSessionPrefixes = (() => {\n\ttry {\n\t\tconst prefixes = JSON.parse(process.env.BYOKIT_APP_OWNED_SESSION_PREFIXES ?? \"[]\");\n\t\treturn Array.isArray(prefixes) && prefixes.every((prefix) => {\n\t\t\tconst agent = typeof prefix === \"string\" && /^agent:([a-z0-9-]+):/.exec(prefix);\n\t\t\treturn agent && !`agent:${agent[1]}:main`.startsWith(prefix);\n\t\t}) ? prefixes : [];\n\t} catch { return []; }\n})();\nfunction isMainRestartRecoveryCandidate(entry, sessionKey) {\n\tif (byokitAppOwnedSessionPrefixes.some((prefix) => sessionKey.startsWith(prefix))) return false;"
|
|
55
55
|
}
|
|
56
56
|
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "dist/prepare.runtime-y2eXKhY3.js",
|
|
60
|
+
"before": "001ebad506ae9550984e3256eb1a632daa2e3ad2b6e76e2ff903979eed3d04fc",
|
|
61
|
+
"after": "f9955de9dbdc30a6675802e940015ad77db3016e84c22a7e282cc5ed4ee350f8",
|
|
62
|
+
"edits": [
|
|
63
|
+
{
|
|
64
|
+
"find": "let systemPrompt = !skipsTurnPreparation ? backendResolved.transformSystemPrompt?.({\n\t\t\tconfig: params.config,\n\t\t\tworkspaceDir,\n\t\t\tprovider: params.provider,\n\t\t\tmodelId,\n\t\t\tmodelDisplay,\n\t\t\tagentId: sessionAgentId,\n\t\t\tsystemPrompt: builtSystemPrompt\n\t\t}) ?? builtSystemPrompt : builtSystemPrompt;\n",
|
|
65
|
+
"replace": "let systemPrompt = !skipsTurnPreparation ? backendResolved.transformSystemPrompt?.({\n\t\t\tconfig: params.config,\n\t\t\tworkspaceDir,\n\t\t\tprovider: params.provider,\n\t\t\tmodelId,\n\t\t\tmodelDisplay,\n\t\t\tagentId: sessionAgentId,\n\t\t\tsystemPrompt: builtSystemPrompt\n\t\t}) ?? builtSystemPrompt : builtSystemPrompt;\n\t\t// BYOKit: Claude wire names for the Tooling list (ch-cli-tool-prefix). The Claude wire catalog\n\t\t// exposes OpenClaw tools as mcp__openclaw__<name>, so the printed Tooling list on that route must\n\t\t// use those exact names. Every other route keeps the stock prompt byte-identical; already-prefixed\n\t\t// names are never prefixed again. Order, summaries and error semantics are unchanged.\n\t\tif (!skipsTurnPreparation && isClaudeCli && promptTools.length > 0) {\n\t\t\tconst byokitBareNames = new Set(promptTools.map((tool) => tool?.name).filter((name) => typeof name === \"string\" && name.length > 0 && !name.startsWith(\"mcp__openclaw__\")));\n\t\t\tif (byokitBareNames.size > 0) {\n\t\t\t\tconst byokitLines = systemPrompt.split(\"\\n\");\n\t\t\t\tlet byokitInTooling = false;\n\t\t\t\tlet byokitToolingArmed = false;\n\t\t\t\tfor (let byokitI = 0; byokitI < byokitLines.length; byokitI++) {\n\t\t\t\t\tconst byokitLine = byokitLines[byokitI];\n\t\t\t\t\tif (byokitLine.startsWith(\"## \")) {\n\t\t\t\t\t\tbyokitInTooling = byokitLine === \"## Tooling\";\n\t\t\t\t\t\tbyokitToolingArmed = false;\n\t\t\t\t\t} else if (byokitInTooling && !byokitToolingArmed) {\n\t\t\t\t\t\tif (byokitLine === \"Tools policy-filtered. Names case-sensitive; call exact.\") byokitToolingArmed = true;\n\t\t\t\t\t} else if (byokitInTooling && byokitToolingArmed && byokitLine.startsWith(\"- \")) {\n\t\t\t\t\t\tconst byokitMatch = /^- (\\S+?)(:.*)?$/.exec(byokitLine);\n\t\t\t\t\t\tif (byokitMatch && byokitBareNames.has(byokitMatch[1])) byokitLines[byokitI] = `- mcp__openclaw__${byokitMatch[1]}${byokitMatch[2] ?? \"\"}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsystemPrompt = byokitLines.join(\"\\n\");\n\t\t\t}\n\t\t}\n"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
57
68
|
}
|
|
58
69
|
]
|
|
59
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@byokit/link": "0.
|
|
64
|
-
"@byokit/relay": "0.
|
|
63
|
+
"@byokit/link": "0.8.0",
|
|
64
|
+
"@byokit/relay": "0.6.0",
|
|
65
65
|
"@byokit/discover": "0.6.0",
|
|
66
66
|
"@byokit/seal": "0.3.0",
|
|
67
67
|
"@byokit/secrets": "0.5.0",
|
|
68
68
|
"@openclaw/gateway-client": "2026.9.6",
|
|
69
69
|
"@openclaw/gateway-protocol": "2026.8.1",
|
|
70
|
-
"ws": "8.
|
|
70
|
+
"ws": "8.22.0"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@byokit/ui-core": "0.6.0"
|