@byokit/openclaw 0.3.3 → 0.3.5
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 +15 -0
- package/README.md +32 -8
- package/dist/auth-status.d.ts +17 -0
- package/dist/auth-status.js +13 -0
- package/dist/auth-store.js +28 -20
- package/dist/engine-status.d.ts +6 -0
- package/dist/engine-status.js +26 -0
- package/dist/engine.d.ts +3 -1
- package/dist/engine.js +68 -13
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/kit.d.ts +2 -5
- package/dist/kit.js +4 -0
- package/dist/link.js +6 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +1 -1
- package/dist/routes.json +18 -9
- package/dist/runs.js +2 -1
- package/dist/signin.d.ts +3 -6
- package/dist/signin.js +88 -14
- package/dist/types.d.ts +12 -3
- package/dist/words.js +2 -0
- package/dist/words.json +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.5 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
- FIX: Failed engine starts no longer remove another live gateway’s guards or saved sign-in state. Verified dead-host gateways stop gracefully before restart; ambiguous ownership returns a typed engine-already-running error and preserves the store.
|
|
10
|
+
|
|
11
|
+
## 0.3.4 (2026-09-30)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
- FIX: Device-code sign-in waits for the engine’s code expiry instead of failing after two minutes; caller cancellation and expiry return typed outcomes with plain words.
|
|
16
|
+
- Offer the native Claude Code / Agent SDK subscription route (`claude-cli`, plugin `anthropic`), keeping login in Claude Code in the isolated engine HOME. Sign-in uses engine detection and live activation; `oc.state` reports native login readiness.
|
|
17
|
+
- Support explicit Anthropic API key activation with a per-use billing label and secret-safe views; never offer an API-key fallback. Direct Claude.ai OAuth and setup-token offers remain excluded.
|
|
18
|
+
- Verify native and API per-run provider/model forwarding, tool events and strict refusal of `@profile` pins.
|
|
19
|
+
|
|
5
20
|
## 0.3.3 (2026-09-30)
|
|
6
21
|
|
|
7
22
|
- Dependency update: pins @byokit/secrets 0.5.0.
|
package/README.md
CHANGED
|
@@ -33,21 +33,36 @@ the pinned engine and needs a real ChatGPT sign-in, so it is not run here):
|
|
|
33
33
|
```ts
|
|
34
34
|
import { OpenClawKit } from '@byokit/openclaw';
|
|
35
35
|
|
|
36
|
-
const kit = new OpenClawKit({ stateDir: './openclaw-state' });
|
|
36
|
+
const kit = new OpenClawKit({ stateDir: './openclaw-state', config: { plugins: { allow: ['openai'] } } });
|
|
37
37
|
await kit.start(); // installs the pinned engine under stateDir, spawns it, connects
|
|
38
38
|
|
|
39
|
-
await kit.ensureMember('
|
|
40
|
-
const
|
|
41
|
-
|
|
39
|
+
await kit.ensureMember('umer');
|
|
40
|
+
const controller = new AbortController(); // call controller.abort() when Umer cancels
|
|
41
|
+
const signIn = kit.signIn('umer', {
|
|
42
|
+
authChoice: 'openai-device-code', via: 'code', signal: controller.signal,
|
|
43
|
+
}, (view) => { if (view.code) console.log('Open', view.url, 'and enter', view.code); });
|
|
44
|
+
const signedIn = await signIn.done;
|
|
45
|
+
if (signedIn.state !== 'done') {
|
|
46
|
+
console.log(signedIn.error); // plain words; why is 'expired' or 'declined' on expiry or cancellation
|
|
47
|
+
await kit.stop();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
42
50
|
|
|
43
51
|
const end = await kit.run(
|
|
44
|
-
{ member: '
|
|
52
|
+
{ member: 'umer', sessionKey: 'agent:umer:main', message: 'Say hello.' },
|
|
45
53
|
(e) => { if (e.type === 'text') console.log(e.text); },
|
|
46
54
|
);
|
|
47
55
|
console.log(end);
|
|
48
56
|
await kit.stop();
|
|
49
57
|
```
|
|
50
58
|
|
|
59
|
+
Device approval can take longer than two minutes. The kit waits through the engine's advertised code lifetime
|
|
60
|
+
(`expiresInMinutes` on the pin, or `expires_in` seconds when supplied), including progress pulls. If the engine
|
|
61
|
+
supplies no lifetime, it owns the deadline. Ordinary wizard requests keep their 120-second timeout.
|
|
62
|
+
`signIn.cancel()` or the optional caller `signal` cancels this sign-in and releases its wizard session.
|
|
63
|
+
`done` returns a typed `SignInView`: `why: 'expired'` for an expired code, `why: 'declined'` for cancellation,
|
|
64
|
+
with a plain sentence in `error`. `SignInOptions` is exported from the host entry.
|
|
65
|
+
|
|
51
66
|
To choose which of a member's accounts a run uses, pass `model: 'provider/model'` in the run spec (for example
|
|
52
67
|
`'openai/gpt-5.1'`, where `provider` is an id `kit.providers(member)` lists). That provider is the one called and billed for this run only, with no fallback to another
|
|
53
68
|
provider or model. If the member isn't signed in to it, the run ends `{ ok: false, kind: 'signed-out' }` and the
|
|
@@ -77,9 +92,9 @@ const kit = new OpenClawKit({ stateDir, spawnEngine: false, transport: fakeGatew
|
|
|
77
92
|
try {
|
|
78
93
|
await kit.start();
|
|
79
94
|
console.log('state:', kit.state.phase, '| protocol:', kit.hello?.protocol);
|
|
80
|
-
await kit.ensureMember('
|
|
95
|
+
await kit.ensureMember('umer');
|
|
81
96
|
const end = await kit.run(
|
|
82
|
-
{ member: '
|
|
97
|
+
{ member: 'umer', sessionKey: 'agent:umer:main', message: 'Say hello.' },
|
|
83
98
|
(e) => console.log('event:', JSON.stringify(e)),
|
|
84
99
|
);
|
|
85
100
|
console.log('end:', JSON.stringify(end));
|
|
@@ -217,7 +232,14 @@ before any restoration; a wrong key, tampering, or a missing adapter rejects. Fi
|
|
|
217
232
|
have mode 0600 and directories 0700. File symlinks are sealed only when their fully resolved targets are
|
|
218
233
|
regular files inside the isolated engine root; they restore as regular files at the link paths. Outside-root,
|
|
219
234
|
dangling and directory symlinks (including loops), sockets, FIFOs and devices are skipped.
|
|
220
|
-
One kit owns the store at a time
|
|
235
|
+
One kit owns the store at a time. After a host crash, preparation stops a verified same-user gateway
|
|
236
|
+
only when its host lock owner is dead, waits up to three seconds for exit, then seals and restores the
|
|
237
|
+
leftover state under the acquired lock. If process identity is ambiguous (including unavailable process
|
|
238
|
+
inspection or a rewritten command line), another host is alive, or shutdown times out, `prepare()` and
|
|
239
|
+
`start()` reject with exported `EngineAlreadyRunningError` (`code: 'engine-already-running'`). The kit reports
|
|
240
|
+
`{ phase: 'failed', why: 'engine-already-running' }`; show `stateWords(kit.state)` and retry after the other
|
|
241
|
+
session stops. Failed-start cleanup and `stop()` on an instance with no ownership preserve the other
|
|
242
|
+
writer's pid, lock, sealed snapshot and live state.
|
|
221
243
|
Sealing copies the whole store through memory, so startup and stop cost grows with session history.
|
|
222
244
|
|
|
223
245
|
The migration doctor temporarily opens the same store and reseals it even when import fails. Once the gateway
|
|
@@ -259,3 +281,5 @@ frozen (docs/runtime-kits.md §5).
|
|
|
259
281
|
## License
|
|
260
282
|
|
|
261
283
|
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
|
|
284
|
+
|
|
285
|
+
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.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GatewayTransport } from './types.ts';
|
|
2
|
+
export type AuthStatus = {
|
|
3
|
+
providers?: (string | {
|
|
4
|
+
provider?: unknown;
|
|
5
|
+
status?: unknown;
|
|
6
|
+
profiles?: {
|
|
7
|
+
status?: unknown;
|
|
8
|
+
}[];
|
|
9
|
+
usage?: unknown;
|
|
10
|
+
})[];
|
|
11
|
+
unavailable?: {
|
|
12
|
+
message?: unknown;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
16
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. */
|
|
17
|
+
export declare function authStatus(request: GatewayTransport['request'], agentId: string, refresh?: boolean, native?: boolean): Promise<AuthStatus>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Native Claude login stays in Claude Code. The pin's profile status does not include this synthetic auth;
|
|
2
|
+
* setup detection asks the CLI itself, without handing credentials to the kit. */
|
|
3
|
+
export async function authStatus(request, agentId, refresh = false, native = false) {
|
|
4
|
+
const status = await request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: 20_000 });
|
|
5
|
+
if (!native)
|
|
6
|
+
return status;
|
|
7
|
+
const detected = await request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 }).catch(() => ({}));
|
|
8
|
+
const rows = (status.providers ?? []).filter((row) => (typeof row === 'string' ? row : row.provider) !== 'claude-cli');
|
|
9
|
+
if (detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true)) {
|
|
10
|
+
return { providers: [...rows, { provider: 'claude-cli', status: 'ok' }] };
|
|
11
|
+
}
|
|
12
|
+
return { ...status, providers: rows };
|
|
13
|
+
}
|
package/dist/auth-store.js
CHANGED
|
@@ -2,20 +2,10 @@
|
|
|
2
2
|
// Seal the complete isolated stores, including journals, only after their writer has exited.
|
|
3
3
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, closeSync, fsyncSync, openSync } from 'node:fs';
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { EngineAlreadyRunningError, pidAlive as live } from "./engine-status.js";
|
|
5
6
|
const encoder = new TextEncoder();
|
|
6
7
|
const archive = (name) => /^(auth-profiles|auth-state|auth|oauth)\.json\.(migrated-.+|sqlite-import\..+\.bak)$/.test(name)
|
|
7
8
|
|| name.endsWith('.moved-to-engine');
|
|
8
|
-
const live = (pid) => {
|
|
9
|
-
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
10
|
-
return false;
|
|
11
|
-
try {
|
|
12
|
-
process.kill(pid, 0);
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
return error.code !== 'ESRCH';
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
9
|
function regular(path) {
|
|
20
10
|
if (!lstatSync(path).isFile())
|
|
21
11
|
throw new Error(`credential store requires regular files: ${path}`);
|
|
@@ -108,18 +98,16 @@ export class AuthStore {
|
|
|
108
98
|
return;
|
|
109
99
|
if (existsSync(this.file) && !this.o.seal)
|
|
110
100
|
throw new Error('authSeal required for sealed credential store');
|
|
111
|
-
if (!this.o.seal)
|
|
112
|
-
return;
|
|
113
101
|
// Never race a live gateway or another kit, including an orphan after its host crashed.
|
|
114
102
|
const gateway = join(this.o.root, 'gateway.pid');
|
|
115
103
|
if (existsSync(gateway) && live(Number(readFileSync(gateway, 'utf8'))))
|
|
116
|
-
throw new
|
|
104
|
+
throw new EngineAlreadyRunningError();
|
|
117
105
|
if (existsSync(this.lock)) {
|
|
118
106
|
if (!lstatSync(this.lock).isDirectory() || lstatSync(this.lock).isSymbolicLink())
|
|
119
107
|
throw new Error('invalid credential lock');
|
|
120
108
|
const owner = join(this.lock, 'pid');
|
|
121
109
|
if (!existsSync(owner) || live(Number(readFileSync(owner, 'utf8'))))
|
|
122
|
-
throw new
|
|
110
|
+
throw new EngineAlreadyRunningError();
|
|
123
111
|
// Only one stale-owner recovery can proceed. Recheck after claiming it so a stale reader
|
|
124
112
|
// cannot remove the lock a newer owner has just acquired.
|
|
125
113
|
const recovery = join(this.lock, 'recovery');
|
|
@@ -127,17 +115,36 @@ export class AuthStore {
|
|
|
127
115
|
mkdirSync(recovery, { mode: 0o700 });
|
|
128
116
|
}
|
|
129
117
|
catch {
|
|
130
|
-
throw new
|
|
118
|
+
throw new EngineAlreadyRunningError();
|
|
131
119
|
}
|
|
132
120
|
if (!existsSync(owner) || live(Number(readFileSync(owner, 'utf8')))) {
|
|
133
121
|
rmSync(recovery, { recursive: true });
|
|
134
|
-
throw new
|
|
122
|
+
throw new EngineAlreadyRunningError();
|
|
135
123
|
}
|
|
136
124
|
rmSync(this.lock, { recursive: true });
|
|
137
125
|
}
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(this.lock, { mode: 0o700 });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code === 'EEXIST')
|
|
131
|
+
throw new EngineAlreadyRunningError();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
140
134
|
this.owned = true;
|
|
135
|
+
try {
|
|
136
|
+
writeFileSync(join(this.lock, 'pid'), String(process.pid), { mode: 0o600, flag: 'wx' });
|
|
137
|
+
// Recheck under the acquired lock before removing a dead writer's guard.
|
|
138
|
+
if (existsSync(gateway)) {
|
|
139
|
+
if (live(Number(readFileSync(gateway, 'utf8'))))
|
|
140
|
+
throw new EngineAlreadyRunningError();
|
|
141
|
+
removeMarker(gateway);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.release();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
141
148
|
}
|
|
142
149
|
release() {
|
|
143
150
|
if (!this.owned)
|
|
@@ -297,7 +304,8 @@ export class AuthStore {
|
|
|
297
304
|
}
|
|
298
305
|
stop() {
|
|
299
306
|
return this.serial(async () => {
|
|
300
|
-
this.
|
|
307
|
+
if (!this.owned)
|
|
308
|
+
return;
|
|
301
309
|
await this.archives();
|
|
302
310
|
await this.persist();
|
|
303
311
|
this.active = false;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A live or unverifiable writer still owns the isolated engine state. Retry after it stops. */
|
|
2
|
+
export declare class EngineAlreadyRunningError extends Error {
|
|
3
|
+
readonly code = "engine-already-running";
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare function pidAlive(pid: number): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/** A live or unverifiable writer still owns the isolated engine state. Retry after it stops. */
|
|
3
|
+
export class EngineAlreadyRunningError extends Error {
|
|
4
|
+
code = 'engine-already-running';
|
|
5
|
+
constructor() {
|
|
6
|
+
super('engine already running; credential store is in use');
|
|
7
|
+
this.name = 'EngineAlreadyRunningError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function pidAlive(pid) {
|
|
11
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
12
|
+
return true; // An invalid guard is ambiguous, never stale.
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
// An orphan can remain a zombie until its new parent reaps it; it has no writer left.
|
|
16
|
+
if (process.platform === 'linux') {
|
|
17
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
18
|
+
if (stat.slice(stat.lastIndexOf(')') + 2).startsWith('Z '))
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return error.code !== 'ESRCH';
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -36,7 +36,8 @@ export declare class Engine {
|
|
|
36
36
|
doctor(timeoutMs: number): {
|
|
37
37
|
status: number | null;
|
|
38
38
|
};
|
|
39
|
-
private
|
|
39
|
+
private verifiedOrphan;
|
|
40
|
+
private recoverOrphan;
|
|
40
41
|
start(): Promise<{
|
|
41
42
|
port: number;
|
|
42
43
|
token: string;
|
|
@@ -45,6 +46,7 @@ export declare class Engine {
|
|
|
45
46
|
private locked;
|
|
46
47
|
private startOnce;
|
|
47
48
|
private launch;
|
|
49
|
+
private removeOwnedPid;
|
|
48
50
|
withAuthStore<T>(task: () => Promise<T>): Promise<T>;
|
|
49
51
|
stop(): Promise<void>;
|
|
50
52
|
}
|
package/dist/engine.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { randomBytes, generateKeyPairSync } from 'node:crypto';
|
|
3
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, copyFileSync, rmSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
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
8
|
import { AuthStore } from "./auth-store.js";
|
|
9
|
+
import { EngineAlreadyRunningError, pidAlive } from "./engine-status.js";
|
|
9
10
|
import { ENGINE_VERSION } from "./constants.js";
|
|
10
11
|
import { reconcileConfig } from "./config.js";
|
|
11
12
|
import { writePlugin, resolveBridge } from "./bridge.js";
|
|
@@ -93,6 +94,7 @@ export class Engine {
|
|
|
93
94
|
async prepareOnce() {
|
|
94
95
|
if (!this.o.authSeal && existsSync(join(this.root, 'auth-store.sealed')))
|
|
95
96
|
throw new Error('authSeal required for sealed credential store');
|
|
97
|
+
await this.recoverOrphan();
|
|
96
98
|
for (const d of [this.root, ...(!this.o.authSeal ? [join(this.root, 'home'), join(this.root, 'state')] : []), join(this.root, 'tmp'), join(this.root, 'install-home'), join(this.root, 'npm-cache'), join(this.o.stateDir, 'logs'), this.dir])
|
|
97
99
|
mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
98
100
|
try {
|
|
@@ -168,19 +170,67 @@ export class Engine {
|
|
|
168
170
|
const result = spawnSync(process.execPath, [entry, 'doctor', '--fix', '--yes', '--non-interactive'], { cwd: env.HOME, env, timeout: timeoutMs, stdio: 'pipe' });
|
|
169
171
|
return { status: result.status };
|
|
170
172
|
}
|
|
171
|
-
|
|
173
|
+
verifiedOrphan(pid) {
|
|
174
|
+
if (process.platform !== 'linux' || !process.getuid)
|
|
175
|
+
return false;
|
|
176
|
+
try {
|
|
177
|
+
const proc = `/proc/${pid}`;
|
|
178
|
+
const cmd = readFileSync(`${proc}/cmdline`, 'utf8').split('\0');
|
|
179
|
+
const env = readFileSync(`${proc}/environ`, 'utf8').split('\0');
|
|
180
|
+
const home = join(this.root, 'home');
|
|
181
|
+
const owner = join(this.root, 'auth-store.lock', 'pid');
|
|
182
|
+
return statSync(proc).uid === process.getuid()
|
|
183
|
+
&& realpathSync(`${proc}/exe`) === realpathSync(process.execPath)
|
|
184
|
+
&& cmd.indexOf(this.entry) > 0 && cmd[cmd.indexOf(this.entry) + 1] === 'gateway'
|
|
185
|
+
&& realpathSync(`${proc}/cwd`) === realpathSync(home)
|
|
186
|
+
&& env.includes(`HOME=${home}`)
|
|
187
|
+
&& env.includes(`OPENCLAW_STATE_DIR=${join(this.root, 'state')}`)
|
|
188
|
+
&& env.includes(`OPENCLAW_CONFIG_PATH=${join(this.root, 'openclaw.json')}`)
|
|
189
|
+
&& existsSync(owner) && !pidAlive(Number(readFileSync(owner, 'utf8')));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async recoverOrphan() {
|
|
196
|
+
if (this.child && this.child.exitCode === null && this.child.signalCode === null)
|
|
197
|
+
return;
|
|
172
198
|
const path = join(this.root, 'gateway.pid');
|
|
173
199
|
if (!existsSync(path))
|
|
174
200
|
return;
|
|
175
201
|
const pid = Number(readFileSync(path, 'utf8'));
|
|
176
|
-
if (!
|
|
177
|
-
return;
|
|
202
|
+
if (!pidAlive(pid))
|
|
203
|
+
return; // AuthStore removes stale guards only under its lock.
|
|
204
|
+
const occupied = () => { this.state('failed', 'engine-already-running'); return new EngineAlreadyRunningError(); };
|
|
205
|
+
if (!this.verifiedOrphan(pid))
|
|
206
|
+
throw occupied();
|
|
207
|
+
// Verify a stable process identity again immediately before signalling this pid, never its group.
|
|
208
|
+
const identity = () => {
|
|
209
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
210
|
+
return stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]; // starttime, not a reusable pid alone
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
const before = identity();
|
|
214
|
+
if (!before || !this.verifiedOrphan(pid) || before !== identity())
|
|
215
|
+
throw occupied();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
if (!pidAlive(pid))
|
|
219
|
+
return;
|
|
220
|
+
throw occupied();
|
|
221
|
+
}
|
|
178
222
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
223
|
+
process.kill(pid, 'SIGTERM');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error.code !== 'ESRCH')
|
|
227
|
+
throw occupied();
|
|
182
228
|
}
|
|
183
|
-
|
|
229
|
+
for (let i = 0; i < 15 && pidAlive(pid); i++)
|
|
230
|
+
await delay(200);
|
|
231
|
+
if (pidAlive(pid))
|
|
232
|
+
throw occupied();
|
|
233
|
+
// The new store owner now seals crash leftovers under its lock before restoring them.
|
|
184
234
|
}
|
|
185
235
|
start() {
|
|
186
236
|
if (this.starting)
|
|
@@ -215,7 +265,9 @@ export class Engine {
|
|
|
215
265
|
return this.launch();
|
|
216
266
|
}
|
|
217
267
|
catch (error) {
|
|
218
|
-
await
|
|
268
|
+
// A launch failure after spawn still has a writer; the caller's stop must await its exit.
|
|
269
|
+
if (!this.child)
|
|
270
|
+
await this.authStore.stop();
|
|
219
271
|
throw error;
|
|
220
272
|
}
|
|
221
273
|
}
|
|
@@ -229,7 +281,6 @@ export class Engine {
|
|
|
229
281
|
return { port: this.port, token: this.token, identityPath };
|
|
230
282
|
if (this.child && this.child.exitCode === null)
|
|
231
283
|
return { port: this.port, token: this.token, identityPath };
|
|
232
|
-
this.stalePid();
|
|
233
284
|
this.state('starting');
|
|
234
285
|
const { entry, env } = this.doctorContext();
|
|
235
286
|
const fd = openSync(join(this.o.stateDir, 'logs', 'openclaw.log'), 'a', 0o600);
|
|
@@ -255,7 +306,7 @@ export class Engine {
|
|
|
255
306
|
if (this.child !== child || this.stopping)
|
|
256
307
|
return;
|
|
257
308
|
this.child = undefined;
|
|
258
|
-
|
|
309
|
+
this.removeOwnedPid(child.pid);
|
|
259
310
|
void (async () => {
|
|
260
311
|
await this.authStore.stop();
|
|
261
312
|
if (this.stopping)
|
|
@@ -275,6 +326,11 @@ export class Engine {
|
|
|
275
326
|
});
|
|
276
327
|
return { port: this.port, token: this.token, identityPath };
|
|
277
328
|
}
|
|
329
|
+
removeOwnedPid(pid) {
|
|
330
|
+
const path = join(this.root, 'gateway.pid');
|
|
331
|
+
if (pid && existsSync(path) && readFileSync(path, 'utf8') === String(pid))
|
|
332
|
+
rmSync(path, { force: true });
|
|
333
|
+
}
|
|
278
334
|
withAuthStore(task) { return this.authStore.offline(task); }
|
|
279
335
|
async stop() {
|
|
280
336
|
this.stopping = true;
|
|
@@ -299,10 +355,9 @@ export class Engine {
|
|
|
299
355
|
if (child && child.exitCode === null && child.signalCode === null)
|
|
300
356
|
throw new Error('engine did not stop; credential store still in use');
|
|
301
357
|
this.child = undefined;
|
|
302
|
-
|
|
358
|
+
this.removeOwnedPid(child?.pid);
|
|
303
359
|
if (!this.credentialsLocked)
|
|
304
360
|
await this.authStore.stop();
|
|
305
|
-
rmSync(this.bridgeSock, { force: true });
|
|
306
361
|
this.state('stopped');
|
|
307
362
|
}
|
|
308
363
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
2
|
+
export type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, RunUsage, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
4
4
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
5
|
+
export { EngineAlreadyRunningError } from './engine-status.ts';
|
package/dist/index.js
CHANGED
package/dist/kit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SealingAdapter } from '@byokit/secrets';
|
|
2
|
-
import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
2
|
+
import type { Approval, CallOptions, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, Member, Route, RunEnd, RunEvent, RunSpec, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
3
3
|
export type KitOptions = {
|
|
4
4
|
stateDir: string;
|
|
5
5
|
authSeal?: SealingAdapter;
|
|
@@ -75,10 +75,7 @@ export declare class OpenClawKit {
|
|
|
75
75
|
routes(): Route[];
|
|
76
76
|
providers(member: Member): Promise<string[]>;
|
|
77
77
|
signedIn(member: Member, provider: string): Promise<boolean>;
|
|
78
|
-
signIn(member: Member, o: {
|
|
79
|
-
authChoice: string;
|
|
80
|
-
via?: 'browser' | 'code';
|
|
81
|
-
}, on: (v: SignInView) => void): {
|
|
78
|
+
signIn(member: Member, o: SignInOptions, on: (v: SignInView) => void): {
|
|
82
79
|
paste(text: string): void;
|
|
83
80
|
cancel(): void;
|
|
84
81
|
done: Promise<SignInView>;
|
package/dist/kit.js
CHANGED
|
@@ -503,6 +503,10 @@ export class OpenClawKit {
|
|
|
503
503
|
if (transport && this.transport === transport)
|
|
504
504
|
await this.disconnect();
|
|
505
505
|
await this.engine.stop();
|
|
506
|
+
if (error instanceof Error && 'code' in error && error.code === 'engine-already-running') {
|
|
507
|
+
this.setState({ phase: 'failed', why: 'engine-already-running' });
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
506
510
|
if (needsUpdate)
|
|
507
511
|
this.setState({ phase: 'needs-update', why: 'version' });
|
|
508
512
|
if (!needsUpdate && !this.stopping)
|
package/dist/link.js
CHANGED
|
@@ -9,6 +9,7 @@ import { reach } from '@byokit/reach';
|
|
|
9
9
|
import { ENGINE_VERSION } from "./constants.js";
|
|
10
10
|
import { b64urlDecode, sealNotice } from "./notices.js";
|
|
11
11
|
import { routeFor } from "./routes.js";
|
|
12
|
+
import { authStatus } from "./auth-status.js";
|
|
12
13
|
import { signedInProviders } from "./runs.js";
|
|
13
14
|
import { stateWords, words } from "./words.js";
|
|
14
15
|
const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events']);
|
|
@@ -99,7 +100,11 @@ export function openclawLink(kit, o) {
|
|
|
99
100
|
const signIns = async (member) => {
|
|
100
101
|
try {
|
|
101
102
|
const { agentId } = await kit.ensureMember(member);
|
|
102
|
-
const status = await
|
|
103
|
+
const status = await authStatus((method, params, options) => {
|
|
104
|
+
if (method === 'models.authStatus')
|
|
105
|
+
return kit.call(method, params, options);
|
|
106
|
+
return kit.call('openclaw.setup.detect', params, options);
|
|
107
|
+
}, agentId, false, true);
|
|
103
108
|
return signedInProviders(status);
|
|
104
109
|
}
|
|
105
110
|
catch {
|
package/dist/routes.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ import type { Route } from './types.ts';
|
|
|
6
6
|
* listed: install-catalog plugins (external providers needing their own install) are not contracted by the pin and
|
|
7
7
|
* are deliberately absent. */
|
|
8
8
|
export declare function routes(): Route[];
|
|
9
|
-
/** The route an app should use: only an offered route counts
|
|
9
|
+
/** The route an app should use: only an offered route counts; API billing is never picked as a fallback. */
|
|
10
10
|
export declare function routeFor(provider: string, via: 'browser' | 'code'): Route | undefined;
|
package/dist/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ const table = routesJson;
|
|
|
10
10
|
export function routes() {
|
|
11
11
|
return table;
|
|
12
12
|
}
|
|
13
|
-
/** The route an app should use: only an offered route counts
|
|
13
|
+
/** The route an app should use: only an offered route counts; API billing is never picked as a fallback. */
|
|
14
14
|
export function routeFor(provider, via) {
|
|
15
15
|
return table.find((route) => route.offer && route.provider === provider && route.via === via);
|
|
16
16
|
}
|
package/dist/routes.json
CHANGED
|
@@ -111,14 +111,17 @@
|
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"choice": "anthropic-cli",
|
|
114
|
-
"provider": "
|
|
114
|
+
"provider": "claude-cli",
|
|
115
115
|
"plugin": "anthropic",
|
|
116
116
|
"billing": "subscription",
|
|
117
117
|
"via": "browser",
|
|
118
|
-
"prerequisite": "
|
|
119
|
-
"offer":
|
|
120
|
-
"reason": "
|
|
121
|
-
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
118
|
+
"prerequisite": "Sign in through unmodified Claude Code on this machine using the kit's isolated HOME and CLAUDE_CONFIG_DIR; the login stays in Claude Code.",
|
|
119
|
+
"offer": true,
|
|
120
|
+
"reason": "Claude subscription via your own Claude Code login on this machine (Agent SDK runtime).",
|
|
121
|
+
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
122
|
+
"auth": "cli",
|
|
123
|
+
"terms": "allowed",
|
|
124
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
122
125
|
},
|
|
123
126
|
{
|
|
124
127
|
"choice": "apiKey",
|
|
@@ -128,8 +131,11 @@
|
|
|
128
131
|
"via": "browser",
|
|
129
132
|
"prerequisite": null,
|
|
130
133
|
"offer": false,
|
|
131
|
-
"reason": "API key (billed per use)
|
|
132
|
-
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
134
|
+
"reason": "API key (billed per use)",
|
|
135
|
+
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
136
|
+
"auth": "api_key",
|
|
137
|
+
"terms": "allowed",
|
|
138
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
133
139
|
},
|
|
134
140
|
{
|
|
135
141
|
"choice": "clawrouter-api-key",
|
|
@@ -348,8 +354,11 @@
|
|
|
348
354
|
"via": "browser",
|
|
349
355
|
"prerequisite": null,
|
|
350
356
|
"offer": false,
|
|
351
|
-
"reason": "
|
|
352
|
-
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)"
|
|
357
|
+
"reason": "Token import is not the native Claude Code login route; direct Claude.ai OAuth remains excluded.",
|
|
358
|
+
"source": "dist/extensions/anthropic/openclaw.plugin.json (2026.8.1)",
|
|
359
|
+
"auth": "token",
|
|
360
|
+
"terms": "grey",
|
|
361
|
+
"termsUrl": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
|
|
353
362
|
},
|
|
354
363
|
{
|
|
355
364
|
"choice": "sglang",
|
package/dist/runs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Runs: one Gateway run per spec, streamed through typed events, ending in a classified RunEnd (5.8, O8).
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { authStatus } from "./auth-status.js";
|
|
4
5
|
import { classify } from "./classify.js";
|
|
5
6
|
// The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
|
|
6
7
|
const WAIT_MS = 3_600_000;
|
|
@@ -125,7 +126,7 @@ export function createRuns(ctx) {
|
|
|
125
126
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
126
127
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
|
127
128
|
// the pinned engine (never another provider or model), so this check plus the override is the guarantee.
|
|
128
|
-
const status = (refresh) => ctx.request
|
|
129
|
+
const status = (refresh) => authStatus(ctx.request, agentId, refresh, picked.provider === 'claude-cli');
|
|
129
130
|
let auth = await status(false);
|
|
130
131
|
if (auth.unavailable)
|
|
131
132
|
auth = await status(true); // no prepared snapshot yet: build it once
|
package/dist/signin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GatewayTransport, Member, SignInView } from './types.ts';
|
|
1
|
+
import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
|
|
2
2
|
export type SignInCtx = {
|
|
3
3
|
request: GatewayTransport['request'];
|
|
4
4
|
ensure(member: Member): Promise<{
|
|
@@ -11,14 +11,11 @@ export type SignInCtx = {
|
|
|
11
11
|
* `paste` hands a returned browser address to a waiting wizard text step. Every exit short of done cancels this
|
|
12
12
|
* session and its callback listener only.
|
|
13
13
|
*/
|
|
14
|
-
export declare function signIn(ctx: SignInCtx, member: Member, o: {
|
|
15
|
-
authChoice: string;
|
|
16
|
-
via?: 'browser' | 'code';
|
|
17
|
-
}, on: (v: SignInView) => void): {
|
|
14
|
+
export declare function signIn(ctx: SignInCtx, member: Member, o: SignInOptions, on: (v: SignInView) => void): {
|
|
18
15
|
paste(text: string): void;
|
|
19
16
|
cancel(): void;
|
|
20
17
|
done: Promise<SignInView>;
|
|
21
18
|
};
|
|
22
|
-
/**
|
|
19
|
+
/** Usable provider profiles plus native Claude Code readiness reported by the engine. */
|
|
23
20
|
export declare function providers(ctx: SignInCtx, member: Member, refresh?: boolean): Promise<string[]>;
|
|
24
21
|
export declare function signOut(ctx: SignInCtx, member: Member, provider: string): Promise<void>;
|
package/dist/signin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Sign-in: the kit drives OpenClaw's own setup wizard loop and holds the ChatGPT callback port during a browser
|
|
2
|
-
// sign-in; credentials
|
|
2
|
+
// sign-in; OAuth credentials stay in the engine/CLI. Explicit API keys go only to engine activation (D11).
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
|
+
import { authStatus } from "./auth-status.js";
|
|
6
|
+
import { signedInProviders } from "./runs.js";
|
|
5
7
|
import { words } from "./words.js";
|
|
6
8
|
// The pin's wizard contract (2026.8.1): steps arrive only from `wizard.next` (wizard.status answers {status, error}
|
|
7
9
|
// and never carries a step), and one session at a time holds the gateway's single setup admission.
|
|
@@ -31,6 +33,18 @@ export function signIn(ctx, member, o, on) {
|
|
|
31
33
|
let settleStart;
|
|
32
34
|
const startingSettled = new Promise((resolve) => { settleStart = resolve; });
|
|
33
35
|
let over = false;
|
|
36
|
+
let approval = false;
|
|
37
|
+
let sawDeviceCode = false;
|
|
38
|
+
let codeExpired = false;
|
|
39
|
+
let codeTimer;
|
|
40
|
+
const stopApproval = () => {
|
|
41
|
+
approval = false;
|
|
42
|
+
if (codeTimer)
|
|
43
|
+
clearTimeout(codeTimer);
|
|
44
|
+
codeTimer = undefined;
|
|
45
|
+
};
|
|
46
|
+
const cancelled = () => ({ state: 'failed', via, why: 'declined', error: words('signin.cancelled') });
|
|
47
|
+
const expired = () => ({ state: 'failed', via, why: 'expired', error: words('signin.expired') });
|
|
34
48
|
let settle;
|
|
35
49
|
let pasteIn;
|
|
36
50
|
let returned;
|
|
@@ -46,7 +60,11 @@ export function signIn(ctx, member, o, on) {
|
|
|
46
60
|
if (over)
|
|
47
61
|
return;
|
|
48
62
|
over = true;
|
|
63
|
+
stopApproval();
|
|
64
|
+
o.signal?.removeEventListener('abort', cancel);
|
|
49
65
|
owner.abort();
|
|
66
|
+
returned = undefined;
|
|
67
|
+
pasteIn = undefined;
|
|
50
68
|
await closeCallback();
|
|
51
69
|
on(view);
|
|
52
70
|
settle(view);
|
|
@@ -63,6 +81,8 @@ export function signIn(ctx, member, o, on) {
|
|
|
63
81
|
return releasing;
|
|
64
82
|
};
|
|
65
83
|
const paste = (text) => {
|
|
84
|
+
if (over)
|
|
85
|
+
return;
|
|
66
86
|
if (pasteIn)
|
|
67
87
|
pasteIn(text);
|
|
68
88
|
else
|
|
@@ -71,9 +91,14 @@ export function signIn(ctx, member, o, on) {
|
|
|
71
91
|
const cancel = () => {
|
|
72
92
|
// Stop the drive now; the start itself is never aborted, and release waits for it to settle before cancelling,
|
|
73
93
|
// so the gateway's one setup admission is freed by a cancel that the engine can actually find.
|
|
94
|
+
if (signal.aborted)
|
|
95
|
+
return;
|
|
74
96
|
owner.abort();
|
|
75
|
-
void (async () => { await release(); await finish(
|
|
97
|
+
void (async () => { await release(); await finish(cancelled()); })();
|
|
76
98
|
};
|
|
99
|
+
o.signal?.addEventListener('abort', cancel, { once: true });
|
|
100
|
+
if (o.signal?.aborted)
|
|
101
|
+
cancel();
|
|
77
102
|
/** Hold 127.0.0.1:<callbackPort> for the sign-in's life; a taken port is why the sign-in cannot start. */
|
|
78
103
|
const holdCallback = () => {
|
|
79
104
|
let took = false;
|
|
@@ -111,8 +136,42 @@ export function signIn(ctx, member, o, on) {
|
|
|
111
136
|
return { state: 'failed', via, why: 'busy' };
|
|
112
137
|
}
|
|
113
138
|
const { agentId } = await ctx.ensure(member);
|
|
114
|
-
//
|
|
115
|
-
//
|
|
139
|
+
// Native login is completed by the person in unmodified Claude Code. The pin does not expose this
|
|
140
|
+
// choice via auth.start; activation verifies the CLI route and persists it only after a live test succeeds.
|
|
141
|
+
if (o.authChoice === 'anthropic-cli' || o.authChoice === 'apiKey') {
|
|
142
|
+
settleStart();
|
|
143
|
+
let apiKey;
|
|
144
|
+
if (o.authChoice === 'anthropic-cli') {
|
|
145
|
+
const detected = await ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: STATUS_MS });
|
|
146
|
+
if (!detected.candidates?.some((candidate) => candidate.kind === 'claude-cli' && candidate.credentials === true))
|
|
147
|
+
return { state: 'failed', via, why: 'failed', error: 'Sign in with Claude Code on this machine in the kit’s isolated HOME, then try again. The login stays in Claude Code.' };
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
say({ prompt: 'API key (billed per use)' });
|
|
151
|
+
apiKey = returned ?? await new Promise((resolve) => {
|
|
152
|
+
const stop = (value) => { clearTimeout(timer); signal.removeEventListener('abort', abort); pasteIn = undefined; resolve(value); };
|
|
153
|
+
const abort = () => stop();
|
|
154
|
+
const timer = setTimeout(abort, PASTE_MS);
|
|
155
|
+
pasteIn = stop;
|
|
156
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
157
|
+
if (signal.aborted)
|
|
158
|
+
abort();
|
|
159
|
+
});
|
|
160
|
+
returned = undefined;
|
|
161
|
+
if (!apiKey)
|
|
162
|
+
return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
|
|
163
|
+
}
|
|
164
|
+
if (signal.aborted)
|
|
165
|
+
return { state: 'failed', via, why: 'declined' };
|
|
166
|
+
const result = await ctx.request('openclaw.setup.activate', {
|
|
167
|
+
agentId, kind: apiKey === undefined ? 'claude-cli' : 'api-key',
|
|
168
|
+
...(apiKey === undefined ? {} : { authChoice: 'apiKey', apiKey }),
|
|
169
|
+
}, { timeoutMs: PULL_MS, signal });
|
|
170
|
+
// API secrets must never be echoed in a view, even if the gateway returns an error containing one.
|
|
171
|
+
return result.ok ? { state: 'done', via } : { state: 'failed', via, why: 'failed',
|
|
172
|
+
error: apiKey === undefined ? cut(result.error ?? 'Claude Code activation failed') : 'API key activation failed' };
|
|
173
|
+
}
|
|
174
|
+
// The client chooses the wizard id; cancellation waits for the engine to register it.
|
|
116
175
|
sessionId = `byokit-${randomUUID()}`;
|
|
117
176
|
let started;
|
|
118
177
|
try {
|
|
@@ -125,8 +184,9 @@ export function signIn(ctx, member, o, on) {
|
|
|
125
184
|
if (started.done)
|
|
126
185
|
return { state: 'done', via };
|
|
127
186
|
// wizard.next, never wizard.status: the status method answers {status, error} and carries no step.
|
|
128
|
-
const
|
|
129
|
-
const
|
|
187
|
+
const options = () => approval ? { timeoutMs: null, signal } : pullOptions(signal);
|
|
188
|
+
const pull = () => ctx.request('wizard.next', { sessionId }, options());
|
|
189
|
+
const answer = (step, value) => ctx.request('wizard.next', { sessionId, answer: { stepId: step.id, ...(value === undefined ? {} : { value }) } }, options());
|
|
130
190
|
const waitForPaste = () => new Promise((resolve) => {
|
|
131
191
|
if (returned !== undefined) {
|
|
132
192
|
const text = returned;
|
|
@@ -166,6 +226,16 @@ export function signIn(ctx, member, o, on) {
|
|
|
166
226
|
const current = step;
|
|
167
227
|
step = undefined;
|
|
168
228
|
if (current.deviceCode) {
|
|
229
|
+
stopApproval();
|
|
230
|
+
approval = true;
|
|
231
|
+
sawDeviceCode = true;
|
|
232
|
+
// The pin converts its provider duration to minutes. Prefer exact seconds if the engine supplies them.
|
|
233
|
+
const duration = current.deviceCode.expires_in !== undefined
|
|
234
|
+
? Number(current.deviceCode.expires_in) * 1_000
|
|
235
|
+
: Number(current.deviceCode.expiresInMinutes) * 60_000;
|
|
236
|
+
if (Number.isFinite(duration) && duration >= 0) {
|
|
237
|
+
codeTimer = setTimeout(() => { codeExpired = true; owner.abort(); }, duration);
|
|
238
|
+
}
|
|
169
239
|
// The code is on the card; acknowledging it lets the engine poll the provider itself.
|
|
170
240
|
say({ ...(current.deviceCode.code ? { code: current.deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}) });
|
|
171
241
|
const next = await answer(current);
|
|
@@ -176,10 +246,12 @@ export function signIn(ctx, member, o, on) {
|
|
|
176
246
|
step = next.step;
|
|
177
247
|
continue;
|
|
178
248
|
}
|
|
249
|
+
if (current.type !== 'progress')
|
|
250
|
+
stopApproval();
|
|
179
251
|
if (current.type === 'text' && !current.sensitive) {
|
|
180
252
|
const value = await waitForPaste();
|
|
181
253
|
if (value === undefined)
|
|
182
|
-
return
|
|
254
|
+
return signal.aborted ? cancelled() : expired();
|
|
183
255
|
const next = await answer(current, value);
|
|
184
256
|
if (next.done) {
|
|
185
257
|
terminal = next;
|
|
@@ -212,10 +284,12 @@ export function signIn(ctx, member, o, on) {
|
|
|
212
284
|
step = next.step;
|
|
213
285
|
}
|
|
214
286
|
if (signal.aborted)
|
|
215
|
-
return
|
|
287
|
+
return codeExpired ? expired() : cancelled();
|
|
216
288
|
if (!terminal)
|
|
217
289
|
return { state: 'failed', via, why: 'expired' };
|
|
218
290
|
const error = String(terminal.error ?? (terminal.status === 'error' ? 'Sign-in failed' : ''));
|
|
291
|
+
if (sawDeviceCode && /expired_token|code.*expired|device.*(?:expired|timed out)/i.test(error))
|
|
292
|
+
return expired();
|
|
219
293
|
return error ? { state: 'failed', via, why: 'failed', error: cut(error) } : { state: 'done', via };
|
|
220
294
|
};
|
|
221
295
|
void (async () => {
|
|
@@ -225,11 +299,13 @@ export function signIn(ctx, member, o, on) {
|
|
|
225
299
|
}
|
|
226
300
|
catch (error) {
|
|
227
301
|
if (signal.aborted)
|
|
228
|
-
view =
|
|
302
|
+
view = codeExpired ? expired() : cancelled();
|
|
303
|
+
else if (sawDeviceCode && /expired_token|code.*expired|device.*(?:expired|timed out)/i.test(cut(error)))
|
|
304
|
+
view = expired();
|
|
229
305
|
else if (BUSY.test(cut(error)))
|
|
230
306
|
view = { state: 'failed', via, why: 'busy' };
|
|
231
307
|
else
|
|
232
|
-
view = { state: 'failed', via, why: 'failed', error: cut(error) };
|
|
308
|
+
view = { state: 'failed', via, why: 'failed', error: o.authChoice === 'apiKey' ? 'API key activation failed' : cut(error) };
|
|
233
309
|
}
|
|
234
310
|
settleStart();
|
|
235
311
|
if (view.state !== 'done')
|
|
@@ -238,12 +314,10 @@ export function signIn(ctx, member, o, on) {
|
|
|
238
314
|
})();
|
|
239
315
|
return { paste, cancel, done };
|
|
240
316
|
}
|
|
241
|
-
/**
|
|
317
|
+
/** Usable provider profiles plus native Claude Code readiness reported by the engine. */
|
|
242
318
|
export async function providers(ctx, member, refresh) {
|
|
243
319
|
const { agentId } = await ctx.ensure(member);
|
|
244
|
-
|
|
245
|
-
const names = (status?.providers ?? []).map((entry) => (typeof entry === 'string' ? entry : entry?.provider));
|
|
246
|
-
return [...new Set(names.filter((name) => typeof name === 'string' && name !== ''))];
|
|
320
|
+
return signedInProviders(await authStatus(ctx.request, agentId, refresh, true)) ?? [];
|
|
247
321
|
}
|
|
248
322
|
export async function signOut(ctx, member, provider) {
|
|
249
323
|
const { agentId } = await ctx.ensure(member);
|
package/dist/types.d.ts
CHANGED
|
@@ -83,11 +83,17 @@ export type RunEnd = {
|
|
|
83
83
|
until?: number;
|
|
84
84
|
message: string;
|
|
85
85
|
};
|
|
86
|
+
export type SignInOptions = {
|
|
87
|
+
authChoice: string;
|
|
88
|
+
via?: 'browser' | 'code';
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
};
|
|
86
91
|
export type SignInView = {
|
|
87
92
|
state: 'waiting' | 'done' | 'failed';
|
|
88
93
|
via: 'browser' | 'code';
|
|
89
94
|
url?: string;
|
|
90
95
|
code?: string;
|
|
96
|
+
prompt?: string;
|
|
91
97
|
error?: string;
|
|
92
98
|
why?: 'busy' | 'declined' | 'expired' | 'failed';
|
|
93
99
|
};
|
|
@@ -109,7 +115,7 @@ export type Decision = {
|
|
|
109
115
|
};
|
|
110
116
|
export type KitState = {
|
|
111
117
|
phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
|
|
112
|
-
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version';
|
|
118
|
+
why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running';
|
|
113
119
|
retryAt?: number;
|
|
114
120
|
};
|
|
115
121
|
export type Hello = {
|
|
@@ -125,6 +131,9 @@ export type Route = {
|
|
|
125
131
|
provider: string;
|
|
126
132
|
plugin: string;
|
|
127
133
|
billing: 'subscription' | 'api' | 'local';
|
|
134
|
+
auth?: 'cli' | 'api_key' | 'token';
|
|
135
|
+
terms?: 'allowed' | 'grey';
|
|
136
|
+
termsUrl?: string;
|
|
128
137
|
via: 'browser' | 'code';
|
|
129
138
|
prerequisite: string | null;
|
|
130
139
|
offer: boolean;
|
|
@@ -134,7 +143,7 @@ export type Route = {
|
|
|
134
143
|
export interface GatewayTransport {
|
|
135
144
|
start(): Promise<Hello>;
|
|
136
145
|
request(method: string, params?: unknown, o?: {
|
|
137
|
-
timeoutMs?: number;
|
|
146
|
+
timeoutMs?: number | null;
|
|
138
147
|
signal?: AbortSignal;
|
|
139
148
|
expectFinal?: boolean;
|
|
140
149
|
onAccepted?: (payload: unknown) => void;
|
|
@@ -147,7 +156,7 @@ export interface GatewayTransport {
|
|
|
147
156
|
stop(): Promise<void>;
|
|
148
157
|
}
|
|
149
158
|
export type CallOptions = {
|
|
150
|
-
timeoutMs?: number;
|
|
159
|
+
timeoutMs?: number | null;
|
|
151
160
|
signal?: AbortSignal;
|
|
152
161
|
};
|
|
153
162
|
export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
|
package/dist/words.js
CHANGED
|
@@ -19,6 +19,8 @@ const PHASE_WORDS = {
|
|
|
19
19
|
};
|
|
20
20
|
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
|
+
if (s.phase === 'failed' && s.why === 'engine-already-running')
|
|
23
|
+
return words('engine.alreadyRunning');
|
|
22
24
|
return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
|
|
23
25
|
}
|
|
24
26
|
export function toAccountView(view, ready) {
|
package/dist/words.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
|
|
6
6
|
"engine.ready": "Ready.",
|
|
7
7
|
"engine.restarting": "Something stopped. Starting it again by itself.",
|
|
8
|
+
"engine.alreadyRunning": "Your saved sign-in is in use. Try again after the other session stops.",
|
|
8
9
|
"engine.failed": "This computer couldn't start the helper. Restart the app to try again.",
|
|
9
10
|
"engine.needsUpdate": "This app needs an update to keep working.",
|
|
10
11
|
"member.signedOut": "Sign in with {name} to start.",
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
"member.network": "Can't reach {name} right now. This keeps trying by itself.",
|
|
14
15
|
"signin.returned": "Thanks. Finishing the sign-in — you can go back to the app now.",
|
|
15
16
|
"signin.busy": "Another sign-in is already in progress. Finish or cancel it, then try again.",
|
|
17
|
+
"signin.cancelled": "Sign-in cancelled. You can start again whenever you are ready.",
|
|
16
18
|
"signin.expired": "The sign-in took too long. Start it again.",
|
|
17
19
|
"approval.ask": "{helper} wants to {summary}. Allow it?",
|
|
18
20
|
"approval.expired": "Nobody answered in time, so this wasn't allowed.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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",
|