@byokit/openclaw 0.6.2 → 0.8.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 +28 -0
- package/README.md +37 -7
- package/SECURITY.md +5 -1
- package/dist/auth-status.d.ts +9 -0
- package/dist/auth-status.js +66 -2
- package/dist/auth-store.d.ts +6 -0
- package/dist/auth-store.js +28 -4
- package/dist/config.js +4 -1
- package/dist/engine-status.d.ts +2 -2
- package/dist/engine-status.js +23 -2
- package/dist/engine.d.ts +2 -1
- package/dist/engine.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kit.d.ts +2 -0
- package/dist/kit.js +31 -6
- package/dist/link.d.ts +2 -2
- package/dist/link.js +2 -18
- package/dist/members.js +13 -2
- package/dist/routes.json +6 -9
- package/dist/runs.d.ts +4 -0
- package/dist/runs.js +21 -7
- package/dist/signin.d.ts +2 -0
- package/dist/signin.js +28 -10
- 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 +12 -1
- package/dist/words.js +4 -0
- package/dist/words.json +2 -0
- package/engine/patches.json +12 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0 (2026-10-09)
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- The deprecated `anthropic` provider alias on route `anthropic-cli` stays readable as `route.deprecatedProvider` (`'anthropic'`); its removal moves to 0.9.0.
|
|
10
|
+
- FIX: `OpenClawKit.abort` always returns a Promise, including after an engine drop; cancellation failures remain catchable without changing healthy abort results.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
## 0.7.0 (2026-10-08)
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
17
|
+
## 0.6.3 (2026-10-07)
|
|
18
|
+
|
|
19
|
+
- Dependency update: pins @byokit/relay 0.5.3.
|
|
20
|
+
- Dependency update: pins @byokit/link 0.7.1.
|
|
21
|
+
|
|
22
|
+
- FIX: Route `anthropic-cli` now reports provider `claude-cli`, the id `signedIn`, `providers()` and runs already use for a Claude Code login, so `kit.signedIn(member, route.provider)` answers true once Claude Code is signed in. **Deprecated:** the old provider `anthropic` on this route; it stays readable as `route.deprecatedProvider` (`'anthropic'`) through 0.7.x and is removed in 0.8.0. Match this route by `provider === 'claude-cli'` (or by `choice === 'anthropic-cli'`). The route id `anthropic:cli:anthropic-cli` is kept through 0.7.x and becomes `claude-cli:cli:anthropic-cli` in 0.8.0; choice `anthropic-cli` and the `{ provider: 'claude-cli', via: 'browser' }` link selector are unchanged. `anthropic` remains the API-billed provider only: a Claude Code login never makes `signedIn(member, 'anthropic')` true.
|
|
23
|
+
FIX: Engine start on macOS no longer sticks on a dead-but-unreaped guard process. `pidAlive` detects zombies portably — `ps` state where there is no `/proc` — instead of Linux-only `/proc`, so a stale guard reads as stopped and start can recover. A live pid still reads as owned, an invalid guard still reads as ambiguous, and anything unexpected from the probe keeps the guard. Linux answers are unchanged.
|
|
24
|
+
- FIX: Code sign-ins whose pinned engine prints the code only into a note now hand the caller the code, link, expiry (`expiresAt`) and instructions (`message`) in the same `SignInView` shape as structured codes, and wait out device approval instead of failing with `gateway request timeout for wizard.next` after two minutes.
|
|
25
|
+
- The O16 day-usage engine tests no longer fail when a run straddles UTC midnight: a case that could still be running at midnight starts just after it, so the day (and month) it reads is the day its engine stamps charges in.
|
|
26
|
+
- 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.
|
|
27
|
+
- FIX: The sealed credential store is written as `v: 1` again. 0.6.2's `v: 2` tag made every earlier kit (through 0.6.1) refuse an intact store with `invalid sealed credential store`, so a host rolled back to an earlier kit could not start and lost its saved sign-in. `v: 2` stores still restore and re-seal as `v: 1`.
|
|
28
|
+
- A saved store the key cannot open (a different key, damaged or tampered bytes) or whose payload is not a credential snapshot no longer blocks engine start: it is kept as `auth-store.sealed.unreadable-<ms>` (never overwritten or deleted), the engine starts signed out, and the kit reports `{ phase: 'ready', why: 'sign-in-reset' }` with the words "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again." until a sign-in through the kit completes.
|
|
29
|
+
- FIX: Every wizard `text` step now reaches the caller as `SignInView.prompt`, not only sensitive ones. A non-sensitive step shows the engine's question as written (`github-copilot-enterprise` asks for the Enterprise domain first), so the sign-in no longer waits 15 minutes on a screen with nothing to answer. Once a step is sensitive (or the choice is `setup-token`), the prompt stays the fixed `Sign-in token` label and no gateway prose is shown, as before.
|
|
30
|
+
- FIX: `setup-token` is no longer offered. The pinned Gateway has no app-guided sign-in for it and refuses every attempt with "That provider setup is not available on this Gateway.", so it is listed with `readiness: 'no_upstream_flow'`, the link no longer maps `{ provider: 'anthropic', via: 'browser' }` to it. `plugins.allow` now holds the plugin of every default-eligible bundled route rather than only the ready ones, so `anthropic-cli` and `apiKey` keep the `anthropic` plugin allowed without app config.
|
|
31
|
+
- The R1 restart-recovery probe accepts both restart-time gate denials for the unowned recovering session (HEAD's `unknown run` and the published kit's `can't check this action right now`); any other transcript text still fails the denial assertion.
|
|
32
|
+
|
|
5
33
|
## 0.6.2 (2026-10-05)
|
|
6
34
|
|
|
7
35
|
FIX: The browser broker now holds its CDP endpoint back until Chromium's initial tab exists, so `/json/list` can never serve an empty browser to an attaching engine on a cold or slow start; a browser that never lists a tab fails the launch honestly instead.
|
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
|
|
@@ -102,6 +104,9 @@ just to read a final result: on cache eviction that would start another run. App
|
|
|
102
104
|
recovery. Typed `kit.call('agent', ...)` remains exact upstream pass-through. Details and real-engine proof are
|
|
103
105
|
in [the run contract](../../docs/runtime-kits.md#58-runs-members-and-streams).
|
|
104
106
|
|
|
107
|
+
Every code route shows its code the same way: a waiting view carries `code`, `url`, `expiresAt` (epoch ms, when
|
|
108
|
+
the engine gives a lifetime) and the engine's own instructions in `message`, including routes whose pinned engine
|
|
109
|
+
prints the code only into a note's text.
|
|
105
110
|
Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
|
|
106
111
|
(`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
|
|
107
112
|
supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
|
|
@@ -109,7 +114,7 @@ supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their
|
|
|
109
114
|
`done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
|
|
110
115
|
with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
|
|
111
116
|
|
|
112
|
-
For
|
|
117
|
+
For any sensitive wizard text step, pass the secret once through the same
|
|
113
118
|
`signIn.paste(token)` channel and clear the input afterwards. The kit displays only a fixed entry label;
|
|
114
119
|
subsequent wizard errors, links and codes are withheld, and failures use a fixed message rather than
|
|
115
120
|
engine text that could echo the token. It is sent only as this member's wizard answer, never in views,
|
|
@@ -127,7 +132,7 @@ For an owner who explicitly offers keys, `kit.addKey('ana', { authChoice: 'opena
|
|
|
127
132
|
`ok`, `invalid` or `not_included`. Label the option **API key (billed per use)**. Select only `routes()` rows with
|
|
128
133
|
`keyEntry: true`; API rows remain `offer: false`. Each route also carries its engine `revision`, `checked` date,
|
|
129
134
|
label and key error word names. Submit the input directly, clear the field afterwards, and never log or save it in
|
|
130
|
-
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`
|
|
131
136
|
for entry, checking, success and failure states, using the kit's `key.*` words.
|
|
132
137
|
|
|
133
138
|
To use the saved key for one run, pass `auth: 'apiKey'`. The key lives in the member's separate
|
|
@@ -139,9 +144,23 @@ because the engine's profile pins can rotate. The `byokit-key-` member prefix is
|
|
|
139
144
|
replaces the previous key; a failed replacement leaves this option unavailable. If the normal agent already has
|
|
140
145
|
an API key added through pass-through calls, `addKey` returns `invalid` and leaves that key untouched.
|
|
141
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
|
+
|
|
142
152
|
A run spec also takes `system`, `images` (`{ data, mimeType }[]`), `thinking` and `tools`, a subset of the app tools
|
|
143
153
|
(`KitOptions.tools` names) this run may call; any other app tool is refused at the gate before `ToolHost.gate` sees
|
|
144
|
-
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
|
|
145
164
|
ends ok carries `usage` (the engine's token total for the run, and `costUsd` when it priced the model) and
|
|
146
165
|
`planWindow` (the subscription's quota windows as the engine last read them), each only when the engine reports it.
|
|
147
166
|
`openclawDevice(link).run(message, o)` takes the same options over the link, and `state()` adds the kit and engine
|
|
@@ -288,7 +307,6 @@ state: ready | protocol: 4
|
|
|
288
307
|
event: {"type":"text","text":"fake: Say hello."}
|
|
289
308
|
event: {"type":"text","text":"fake: Say hello."}
|
|
290
309
|
end: {"ok":true,"text":"fake: Say hello.","usage":{"input":10,"output":16,"total":26}}
|
|
291
|
-
route: setup-token subscription setup_token
|
|
292
310
|
route: github-copilot subscription code
|
|
293
311
|
route: github-copilot-enterprise subscription code
|
|
294
312
|
route: minimax-global-oauth subscription code
|
|
@@ -329,7 +347,7 @@ Entries:
|
|
|
329
347
|
| `RouteView`, `RouteFacts`, `JsonValue`, `UsageClient`, `DayUsageClient` (`.`) | The types `routes()`, `routes(facts)` and the usage readers return or take, nameable without a cast |
|
|
330
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 |
|
|
331
349
|
| `LinkRefused`, `openNotice` (`./device`) | The host's own refusal as an error; opens a sealed approval notice |
|
|
332
|
-
| `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` |
|
|
333
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 |
|
|
334
352
|
| `serve(o)` (`./link`) | Binds the link host per reach and returns its URLs (`OpenClawServeOptions` / `OpenClawServeHandle`) |
|
|
335
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` |
|
|
@@ -481,7 +499,19 @@ separately; dual-wrap rotation retains old wrapping keys for backups.
|
|
|
481
499
|
Without `authSeal`, engine credentials remain plaintext. With it, successful `prepare()` and `stop()` leave
|
|
482
500
|
only a sealed snapshot, `auth-store.sealed`, for the sealed credential paths; regenerable caches stay on
|
|
483
501
|
disk unsealed. The adapter authenticates the snapshot
|
|
484
|
-
before any restoration; a
|
|
502
|
+
before any restoration; a missing adapter rejects. A store the key cannot open (a wrong key,
|
|
503
|
+
damaged or tampered bytes) or whose payload is not a credential snapshot stays unchanged at
|
|
504
|
+
`auth-store.sealed`. Both `prepare()` and `start()` reject with exported `AuthStoreUnreadableError`
|
|
505
|
+
(`code: 'auth-store-unreadable'`, `reason: 'auth-failed' | 'invalid-snapshot'`). The kit reports
|
|
506
|
+
`{ phase: 'failed', why: 'auth-store-unreadable' }`; show `stateWords(kit.state)`, not a sign-in prompt.
|
|
507
|
+
Restore access to the original seal/key (and the original service/stateDir binding if the adapter uses
|
|
508
|
+
one), then retry `start()` with that adapter. Do not generate/rotate a key to repair an unreadable store.
|
|
509
|
+
If the bytes are damaged, stop all writers, acquire the host writer lock, and restore an authentic
|
|
510
|
+
backup with its matching key before retrying. There is no automatic reset or overwrite.
|
|
511
|
+
For stores kept aside by an older kit, restore the original `.unreadable-*` snapshot to
|
|
512
|
+
`auth-store.sealed` only while stopped under that lock, preserving both it and any replacement as
|
|
513
|
+
backups first; use the original seal. Without the matching key or an authentic backup the kit cannot
|
|
514
|
+
recover the sign-in. Files restored for the engine
|
|
485
515
|
have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
|
|
486
516
|
regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
|
|
487
517
|
dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
|
|
@@ -536,4 +566,4 @@ frozen (docs/runtime-kits.md §5).
|
|
|
536
566
|
|
|
537
567
|
Apache-2.0. See [LICENSE](LICENSE); the bundled engine's MIT text is [engine/OPENCLAW-LICENSE](engine/OPENCLAW-LICENSE).
|
|
538
568
|
|
|
539
|
-
Claude: `anthropic-cli` uses your own unmodified Claude Code login on this machine (provider `claude-cli`, plugin `anthropic`; subscription billing), 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 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.
|
|
569
|
+
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
|
@@ -28,7 +28,11 @@ resolve with `phase: 'locked'`, do not restore plaintext or launch the engine, a
|
|
|
28
28
|
retries after unlock. Opt-in dual wrapping opens through an owner-only host key without prompting;
|
|
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
|
-
the replacement decrypts to identical contents.
|
|
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, 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.
|
|
32
36
|
|
|
33
37
|
## Secrets at rest and in memory
|
|
34
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,6 +1,12 @@
|
|
|
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
11
|
private active;
|
|
6
12
|
private owned;
|
package/dist/auth-store.js
CHANGED
|
@@ -88,6 +88,16 @@ 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
102
|
active = false;
|
|
93
103
|
owned = false;
|
|
@@ -174,8 +184,22 @@ export class AuthStore {
|
|
|
174
184
|
regular(this.file);
|
|
175
185
|
const seal = this.o.seal;
|
|
176
186
|
const bytes = readFileSync(this.file);
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
let text;
|
|
188
|
+
try {
|
|
189
|
+
text = seal.decryptString(bytes);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error?.code !== 'auth-failed')
|
|
193
|
+
throw error;
|
|
194
|
+
throw new AuthStoreUnreadableError('auth-failed');
|
|
195
|
+
}
|
|
196
|
+
let saved;
|
|
197
|
+
try {
|
|
198
|
+
saved = snapshot(text);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw new AuthStoreUnreadableError('invalid-snapshot');
|
|
202
|
+
}
|
|
179
203
|
const upgraded = seal.upgrade?.(bytes);
|
|
180
204
|
if (upgraded) {
|
|
181
205
|
if (seal.decryptString(Buffer.from(upgraded)) !== text)
|
|
@@ -185,7 +209,7 @@ export class AuthStore {
|
|
|
185
209
|
return saved;
|
|
186
210
|
}
|
|
187
211
|
collect() {
|
|
188
|
-
const s = { v:
|
|
212
|
+
const s = { v: 1, dirs: [], files: [] };
|
|
189
213
|
const root = realpathSync(this.o.root);
|
|
190
214
|
const walk = (path) => {
|
|
191
215
|
const name = relative(this.o.root, path).split('\\').join('/');
|
|
@@ -272,7 +296,7 @@ export class AuthStore {
|
|
|
272
296
|
put(this.file, sealed);
|
|
273
297
|
if (this.o.seal.decryptString(readFileSync(this.file)) !== payload)
|
|
274
298
|
throw new Error('credential seal verification failed');
|
|
275
|
-
if (previous?.
|
|
299
|
+
if (previous?.dirs.some(cached))
|
|
276
300
|
this.o.log?.('sealed credential store re-sealed: tool caches no longer sealed');
|
|
277
301
|
put(this.cleanup, encoder.encode('1'));
|
|
278
302
|
this.remove(saved);
|
package/dist/config.js
CHANGED
|
@@ -56,7 +56,10 @@ export function reconcileConfig(saved, o) {
|
|
|
56
56
|
c.plugins.load ??= {};
|
|
57
57
|
c.plugins.load.paths = [...new Set([...(c.plugins.load.paths ?? []).filter((p) => !p.includes('byokit-openclaw-bridge')), o.pluginDir])];
|
|
58
58
|
c.plugins.allow = [...new Set([...(c.plugins.allow ?? []), o.pluginId,
|
|
59
|
-
|
|
59
|
+
// Every default-eligible bundled route, not only the ready ones: anthropic-cli waits on its binary and
|
|
60
|
+
// shares its plugin with the explicit apiKey route, so neither may depend on another route being offered.
|
|
61
|
+
...routes().filter(route => route.offerPolicy === 'default' && !route.needs?.plugin && route.plugin)
|
|
62
|
+
.map(route => route.plugin)])];
|
|
60
63
|
c.plugins.entries ??= {};
|
|
61
64
|
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
62
65
|
if (o.browser) {
|
package/dist/engine-status.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
2
|
/** Every process the kit itself spawned, held for shutdown by pid. */
|
|
3
3
|
export declare class StartedProcesses {
|
|
4
4
|
private readonly started;
|
|
@@ -16,4 +16,4 @@ export declare class EngineAlreadyRunningError extends Error {
|
|
|
16
16
|
readonly code = "engine-already-running";
|
|
17
17
|
constructor();
|
|
18
18
|
}
|
|
19
|
-
export declare function pidAlive(pid: number): boolean;
|
|
19
|
+
export declare function pidAlive(pid: number, platform?: NodeJS.Platform): boolean;
|
package/dist/engine-status.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
/** Every process the kit itself spawned, held for shutdown by pid. */
|
|
3
4
|
export class StartedProcesses {
|
|
4
5
|
started = new Set();
|
|
@@ -38,20 +39,40 @@ export class EngineAlreadyRunningError extends Error {
|
|
|
38
39
|
this.name = 'EngineAlreadyRunningError';
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
export function pidAlive(pid) {
|
|
42
|
+
export function pidAlive(pid, platform = process.platform) {
|
|
42
43
|
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
43
44
|
return true; // An invalid guard is ambiguous, never stale.
|
|
44
45
|
try {
|
|
45
46
|
process.kill(pid, 0);
|
|
46
47
|
// An orphan can remain a zombie until its new parent reaps it; it has no writer left.
|
|
47
|
-
if (
|
|
48
|
+
if (platform === 'linux') {
|
|
48
49
|
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
49
50
|
if (stat.slice(stat.lastIndexOf(')') + 2).startsWith('Z '))
|
|
50
51
|
return false;
|
|
51
52
|
}
|
|
53
|
+
else if (platform !== 'win32' && psReportsZombie(pid)) {
|
|
54
|
+
// No /proc here (notably macOS): only a definite zombie reads as stale.
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
52
57
|
return true;
|
|
53
58
|
}
|
|
54
59
|
catch (error) {
|
|
55
60
|
return error.code !== 'ESRCH';
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Why ps: `ps -o stat=` exists on macOS and Linux with the same leading-letter state
|
|
65
|
+
* convention, so a `Z` first letter names the same kernel fact the /proc read names on
|
|
66
|
+
* Linux. Linux keeps its /proc read (no subprocess, unchanged answers); every other Unix
|
|
67
|
+
* asks ps. Only positive `Z` evidence flips the answer — a missing ps, an empty reply,
|
|
68
|
+
* or gibberish keeps the guard, so a reused live pid is never stolen.
|
|
69
|
+
*/
|
|
70
|
+
function psReportsZombie(pid) {
|
|
71
|
+
try {
|
|
72
|
+
const probed = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8', timeout: 5000 });
|
|
73
|
+
return (probed.stdout ?? '').trimStart().startsWith('Z');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthStore } from './auth-store.ts';
|
|
1
2
|
import type { KitOptions } from './kit.ts';
|
|
2
3
|
import type { KitState, ToolSpec } from './types.ts';
|
|
3
4
|
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal'> & {
|
|
@@ -24,7 +25,7 @@ export declare class Engine {
|
|
|
24
25
|
private child?;
|
|
25
26
|
private readonly started;
|
|
26
27
|
private starting?;
|
|
27
|
-
|
|
28
|
+
readonly authStore: AuthStore;
|
|
28
29
|
private stopping;
|
|
29
30
|
private credentialsLocked;
|
|
30
31
|
private repaired;
|
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
|
@@ -6,6 +6,7 @@ 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
|
@@ -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): {
|
package/dist/kit.js
CHANGED
|
@@ -11,11 +11,12 @@ 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";
|
|
@@ -428,6 +429,7 @@ export class OpenClawKit {
|
|
|
428
429
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
429
430
|
bridge;
|
|
430
431
|
keys;
|
|
432
|
+
admission;
|
|
431
433
|
approvalsCtl;
|
|
432
434
|
listeners = new Set();
|
|
433
435
|
off = [];
|
|
@@ -644,6 +646,7 @@ export class OpenClawKit {
|
|
|
644
646
|
...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
|
|
645
647
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
646
648
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
649
|
+
this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
|
|
647
650
|
this.keys = createKeys({ root: this.engine.root,
|
|
648
651
|
restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
|
|
649
652
|
request: (method, params, options) => this.request()(method, params, options),
|
|
@@ -684,6 +687,8 @@ export class OpenClawKit {
|
|
|
684
687
|
slot.bridge = this.bridge;
|
|
685
688
|
}
|
|
686
689
|
setState(s) {
|
|
690
|
+
if (s.phase !== 'ready')
|
|
691
|
+
this.admission?.invalidate();
|
|
687
692
|
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
688
693
|
this.o.onState?.(this.current);
|
|
689
694
|
}
|
|
@@ -753,6 +758,10 @@ export class OpenClawKit {
|
|
|
753
758
|
this.setState({ phase: 'failed', why: 'engine-already-running' });
|
|
754
759
|
throw error;
|
|
755
760
|
}
|
|
761
|
+
if (error instanceof Error && 'code' in error && error.code === 'auth-store-unreadable') {
|
|
762
|
+
this.setState({ phase: 'failed', why: 'auth-store-unreadable' });
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
756
765
|
if (patchFailure) {
|
|
757
766
|
this.setState({ phase: 'failed', why: 'engine-patch' });
|
|
758
767
|
throw error;
|
|
@@ -765,6 +774,7 @@ export class OpenClawKit {
|
|
|
765
774
|
}
|
|
766
775
|
}
|
|
767
776
|
async disconnect() {
|
|
777
|
+
this.admission.invalidate();
|
|
768
778
|
this.off.splice(0).forEach((fn) => fn());
|
|
769
779
|
const transport = this.transport;
|
|
770
780
|
this.transport = undefined;
|
|
@@ -812,7 +822,14 @@ export class OpenClawKit {
|
|
|
812
822
|
request() {
|
|
813
823
|
if (!this.transport || this.current.phase !== 'ready')
|
|
814
824
|
throw new Error('gateway not ready');
|
|
815
|
-
|
|
825
|
+
const transport = this.transport;
|
|
826
|
+
return (method, params, options) => {
|
|
827
|
+
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);
|
|
828
|
+
if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
|
|
829
|
+
return transport.request(method, params, options);
|
|
830
|
+
this.admission.invalidate();
|
|
831
|
+
return Promise.resolve().then(() => transport.request(method, params, options)).finally(() => this.admission.invalidate());
|
|
832
|
+
};
|
|
816
833
|
}
|
|
817
834
|
call(method, params, o) {
|
|
818
835
|
if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
|
|
@@ -869,17 +886,23 @@ export class OpenClawKit {
|
|
|
869
886
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
870
887
|
signInCtx() {
|
|
871
888
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
889
|
+
// Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
|
|
890
|
+
authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
|
|
872
891
|
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
873
892
|
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
|
|
874
893
|
}
|
|
875
894
|
routes() {
|
|
876
895
|
return routeTable();
|
|
877
896
|
}
|
|
897
|
+
async providerStatus(member) {
|
|
898
|
+
const { agentId } = await this.ensureMember(member);
|
|
899
|
+
return signedInProviders(await this.admission.read(agentId, false, true, false));
|
|
900
|
+
}
|
|
878
901
|
providers(member) {
|
|
879
|
-
return
|
|
902
|
+
return this.providerStatus(member).then((names) => names ?? []);
|
|
880
903
|
}
|
|
881
904
|
signedIn(member, provider) {
|
|
882
|
-
return
|
|
905
|
+
return this.providers(member).then((names) => names.includes(provider)).catch(() => false);
|
|
883
906
|
}
|
|
884
907
|
signIn(member, o, on) {
|
|
885
908
|
return startSignIn(this.signInCtx(), member, o, on);
|
|
@@ -903,6 +926,8 @@ export class OpenClawKit {
|
|
|
903
926
|
runs() {
|
|
904
927
|
return createRuns({
|
|
905
928
|
request: (method, params, o) => this.request()(method, params, o),
|
|
929
|
+
authStatus: this.admission.read,
|
|
930
|
+
invalidateAuth: this.admission.invalidate,
|
|
906
931
|
onEvent: (fn) => {
|
|
907
932
|
this.listeners.add(fn);
|
|
908
933
|
return () => {
|
|
@@ -960,7 +985,7 @@ export class OpenClawKit {
|
|
|
960
985
|
steer(sessionKey, text, o) {
|
|
961
986
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
962
987
|
}
|
|
963
|
-
abort(sessionKey, o) {
|
|
988
|
+
async abort(sessionKey, o) {
|
|
964
989
|
return this.runs().abort(this.runKey(sessionKey, o));
|
|
965
990
|
}
|
|
966
991
|
// approvals (5.9)
|
package/dist/link.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BrowserHost } from './browser.ts';
|
|
2
2
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
3
3
|
import { type Grant, type Host, type HostOptions } from '@byokit/link';
|
|
4
|
-
import { type ServeIngress, type Via } from '@byokit/
|
|
4
|
+
import { type ServeIngress, type Via } from '@byokit/discover';
|
|
5
5
|
import type { PushAction, RelayClient } from '@byokit/relay';
|
|
6
6
|
import type { OpenClawKit } from './kit.ts';
|
|
7
7
|
import type { Member } from './types.ts';
|
|
@@ -9,7 +9,7 @@ import type { Member } from './types.ts';
|
|
|
9
9
|
export type OpenClawLinkOptions = {
|
|
10
10
|
memberOf: (grant: Grant) => Member | undefined;
|
|
11
11
|
passThrough?: (method: string, grant: Grant) => boolean;
|
|
12
|
-
relay?: RelayClient
|
|
12
|
+
relay?: Pick<RelayClient, 'notify'>;
|
|
13
13
|
};
|
|
14
14
|
/** What `openclawLink` hands the link host: its request/stream handlers plus the kit's push entry point. */
|
|
15
15
|
export type OpenClawLinkHost = Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
|
package/dist/link.js
CHANGED
|
@@ -6,13 +6,11 @@ import { readFileSync } from 'node:fs';
|
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { WebSocketServer } from 'ws';
|
|
8
8
|
import { PublicLinkError, } from '@byokit/link';
|
|
9
|
-
import { reach } from '@byokit/
|
|
9
|
+
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/routes.json
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"choice": "anthropic-cli",
|
|
125
|
-
"provider": "
|
|
125
|
+
"provider": "claude-cli",
|
|
126
126
|
"plugin": "anthropic",
|
|
127
127
|
"billing": "subscription",
|
|
128
128
|
"via": "cli",
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
"provider": "claude-cli",
|
|
135
135
|
"via": "browser"
|
|
136
136
|
},
|
|
137
|
+
"deprecatedProvider": "anthropic",
|
|
137
138
|
"reason": "Anthropic Claude CLI: subscription sign-in.",
|
|
138
139
|
"source": "extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
139
140
|
"revision": "2026.8.1",
|
|
@@ -173,13 +174,9 @@
|
|
|
173
174
|
"via": "setup_token",
|
|
174
175
|
"auth": "token",
|
|
175
176
|
"prerequisite": null,
|
|
176
|
-
"offer":
|
|
177
|
-
"offerPolicy": "
|
|
178
|
-
"
|
|
179
|
-
"provider": "anthropic",
|
|
180
|
-
"via": "browser"
|
|
181
|
-
},
|
|
182
|
-
"reason": "Anthropic setup-token: subscription sign-in.",
|
|
177
|
+
"offer": false,
|
|
178
|
+
"offerPolicy": "explicit",
|
|
179
|
+
"reason": "The pinned Gateway has no app-guided sign-in for this choice.",
|
|
183
180
|
"source": "extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
184
181
|
"revision": "2026.8.1",
|
|
185
182
|
"checked": "2026-10-02",
|
|
@@ -201,7 +198,7 @@
|
|
|
201
198
|
"id": "anthropic",
|
|
202
199
|
"method": "setup-token",
|
|
203
200
|
"revision": "ea806575e6450e4d1efdfc72c19f04be982a1b9b",
|
|
204
|
-
"flow": "
|
|
201
|
+
"flow": "absent"
|
|
205
202
|
}
|
|
206
203
|
},
|
|
207
204
|
{
|
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
|
@@ -15,6 +15,19 @@ const CANCEL_MS = 10_000;
|
|
|
15
15
|
const RECONFIRM_MS = 60_000;
|
|
16
16
|
const TURNS = 200;
|
|
17
17
|
const PASTE_MS = 15 * 60_000;
|
|
18
|
+
// Two of the pin's code routes print their code only into a note's text ("Code: X" with
|
|
19
|
+
// "Code expires in N minutes", or "enter the code X." with "Expires at: <ISO>"), never as `deviceCode`.
|
|
20
|
+
// Read it back so every code route reaches the caller in the one structured shape.
|
|
21
|
+
const noteCode = (step) => {
|
|
22
|
+
const text = step.type === 'note' ? step.message ?? '' : '';
|
|
23
|
+
const code = /^Code: (\S+)$/m.exec(text)?.[1] ?? /enter the code (\S+?)\.?$/m.exec(text)?.[1];
|
|
24
|
+
if (!code)
|
|
25
|
+
return undefined;
|
|
26
|
+
const minutes = /Code expires in (\d+) minutes/.exec(text)?.[1];
|
|
27
|
+
const at = Date.parse(/Expires at: (\S+)/.exec(text)?.[1] ?? '');
|
|
28
|
+
return { code, message: text, ...(minutes ? { expiresInMinutes: Number(minutes) }
|
|
29
|
+
: Number.isFinite(at) ? { expires_in: Math.max(0, Math.floor((at - Date.now()) / 1_000)) } : {}) };
|
|
30
|
+
};
|
|
18
31
|
const cut = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 200);
|
|
19
32
|
const pullOptions = (signal) => ({ timeoutMs: PULL_MS, signal });
|
|
20
33
|
/**
|
|
@@ -268,19 +281,22 @@ export function signIn(ctx, member, o, on) {
|
|
|
268
281
|
step = undefined;
|
|
269
282
|
if (current.type === 'text' && current.sensitive)
|
|
270
283
|
sensitive = true;
|
|
271
|
-
|
|
284
|
+
const deviceCode = current.type === 'text' ? undefined : current.deviceCode ?? noteCode(current);
|
|
285
|
+
if (deviceCode) {
|
|
272
286
|
stopApproval();
|
|
273
287
|
approval = true;
|
|
274
288
|
sawDeviceCode = true;
|
|
275
289
|
// The pin converts its provider duration to minutes. Prefer exact seconds if the engine supplies them.
|
|
276
|
-
const duration =
|
|
277
|
-
? Number(
|
|
278
|
-
: Number(
|
|
279
|
-
|
|
290
|
+
const duration = deviceCode.expires_in !== undefined
|
|
291
|
+
? Number(deviceCode.expires_in) * 1_000
|
|
292
|
+
: Number(deviceCode.expiresInMinutes) * 60_000;
|
|
293
|
+
const timed = Number.isFinite(duration) && duration >= 0;
|
|
294
|
+
if (timed)
|
|
280
295
|
codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
|
|
281
|
-
|
|
296
|
+
const message = deviceCode.message ?? current.message;
|
|
282
297
|
// The code is on the card; acknowledging it lets the engine poll the provider itself.
|
|
283
|
-
say({ ...(
|
|
298
|
+
say({ ...(deviceCode.code ? { code: deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}),
|
|
299
|
+
...(timed ? { expiresAt: Date.now() + duration } : {}), ...(message ? { message } : {}) });
|
|
284
300
|
const next = await answer(current);
|
|
285
301
|
if (next.done) {
|
|
286
302
|
terminal = next;
|
|
@@ -292,8 +308,9 @@ export function signIn(ctx, member, o, on) {
|
|
|
292
308
|
if (current.type !== 'progress')
|
|
293
309
|
stopApproval();
|
|
294
310
|
if (current.type === 'text') {
|
|
295
|
-
|
|
296
|
-
|
|
311
|
+
// Every text step is a question the person must answer, so it is always asked. A non-sensitive one is not a
|
|
312
|
+
// secret by the engine's own flag: its question is shown as written. Once anything is sensitive, only the fixed label is.
|
|
313
|
+
say({ prompt: sensitive ? 'Sign-in token' : current.message || 'Paste what the sign-in page gave you' });
|
|
297
314
|
const value = await waitForPaste();
|
|
298
315
|
if (value === undefined)
|
|
299
316
|
return signal.aborted ? cancelled() : expired();
|
|
@@ -413,7 +430,8 @@ export function signIn(ctx, member, o, on) {
|
|
|
413
430
|
/** Usable provider profiles plus native Claude Code readiness reported by the engine. */
|
|
414
431
|
export async function providers(ctx, member, refresh) {
|
|
415
432
|
const { agentId } = await ctx.ensure(member);
|
|
416
|
-
return signedInProviders(await
|
|
433
|
+
return signedInProviders(await (ctx.authStatus ? ctx.authStatus(agentId, refresh ?? false, true)
|
|
434
|
+
: authStatus(ctx.request, agentId, refresh, true))) ?? [];
|
|
417
435
|
}
|
|
418
436
|
export async function signOut(ctx, member, provider) {
|
|
419
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
|
@@ -47,6 +47,11 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
|
|
|
47
47
|
register?: boolean;
|
|
48
48
|
}
|
|
49
49
|
export type RunEvent = {
|
|
50
|
+
type: 'started';
|
|
51
|
+
} | {
|
|
52
|
+
type: 'thinking';
|
|
53
|
+
tokens: number;
|
|
54
|
+
} | {
|
|
50
55
|
type: 'text';
|
|
51
56
|
text: string;
|
|
52
57
|
} | {
|
|
@@ -103,6 +108,8 @@ export type SignInView = {
|
|
|
103
108
|
via: 'browser' | 'code';
|
|
104
109
|
url?: string;
|
|
105
110
|
code?: string;
|
|
111
|
+
expiresAt?: number;
|
|
112
|
+
message?: string;
|
|
106
113
|
prompt?: string;
|
|
107
114
|
error?: string;
|
|
108
115
|
why?: 'busy' | 'declined' | 'expired' | 'failed';
|
|
@@ -125,7 +132,7 @@ export type Decision = {
|
|
|
125
132
|
};
|
|
126
133
|
export type KitState = {
|
|
127
134
|
phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
|
|
128
|
-
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch';
|
|
135
|
+
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset' | 'auth-store-unreadable';
|
|
129
136
|
retryAt?: number;
|
|
130
137
|
patchSet?: string | null;
|
|
131
138
|
};
|
|
@@ -157,6 +164,10 @@ export type Route = {
|
|
|
157
164
|
provider: string;
|
|
158
165
|
via: 'browser' | 'code';
|
|
159
166
|
};
|
|
167
|
+
/** @deprecated This route's `provider` before 0.7.0 (`anthropic` on `anthropic-cli`); use `provider` (`claude-cli`).
|
|
168
|
+
* Discovery data only, never a sign-in or `signedIn` id. Removed in 0.9.0, when that route's id becomes
|
|
169
|
+
* `claude-cli:cli:anthropic-cli`. */
|
|
170
|
+
deprecatedProvider?: string;
|
|
160
171
|
billingFrom?: 'source' | 'host';
|
|
161
172
|
group?: 'models' | 'services';
|
|
162
173
|
platforms?: {
|
package/dist/words.js
CHANGED
|
@@ -21,6 +21,10 @@ 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');
|
|
26
|
+
if (s.why === 'sign-in-reset')
|
|
27
|
+
return words('engine.signInAgain');
|
|
24
28
|
return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
|
|
25
29
|
}
|
|
26
30
|
export function toAccountView(view, ready) {
|
package/dist/words.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
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.",
|
|
7
|
+
"engine.signInAgain": "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again.",
|
|
6
8
|
"engine.ready": "Ready.",
|
|
7
9
|
"engine.restarting": "Something stopped. Starting it again by itself.",
|
|
8
10
|
"engine.alreadyRunning": "Your saved sign-in is in use. Try again after the other session stops.",
|
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.8.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,9 +60,9 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@byokit/link": "0.7.
|
|
64
|
-
"@byokit/relay": "0.5.
|
|
65
|
-
"@byokit/
|
|
63
|
+
"@byokit/link": "0.7.1",
|
|
64
|
+
"@byokit/relay": "0.5.3",
|
|
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",
|