@orgloop/agentctl 1.0.1 → 1.2.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/README.md +145 -3
- package/dist/adapters/claude-code.js +10 -10
- package/dist/adapters/codex.d.ts +72 -0
- package/dist/adapters/codex.js +692 -0
- package/dist/adapters/openclaw.d.ts +60 -9
- package/dist/adapters/openclaw.js +195 -38
- package/dist/adapters/opencode.d.ts +143 -0
- package/dist/adapters/opencode.js +672 -0
- package/dist/adapters/pi-rust.d.ts +89 -0
- package/dist/adapters/pi-rust.js +743 -0
- package/dist/adapters/pi.d.ts +96 -0
- package/dist/adapters/pi.js +855 -0
- package/dist/cli.js +277 -59
- package/dist/core/types.d.ts +1 -0
- package/dist/daemon/server.js +34 -4
- package/dist/daemon/session-tracker.d.ts +20 -0
- package/dist/daemon/session-tracker.js +150 -4
- package/dist/daemon/state.d.ts +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +4 -0
- package/dist/launch-orchestrator.d.ts +60 -0
- package/dist/launch-orchestrator.js +198 -0
- package/dist/matrix-parser.d.ts +40 -0
- package/dist/matrix-parser.js +69 -0
- package/dist/utils/partial-read.d.ts +20 -0
- package/dist/utils/partial-read.js +66 -0
- package/dist/worktree.d.ts +22 -0
- package/dist/worktree.js +68 -0
- package/package.json +3 -2
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
|
+
export interface DeviceIdentity {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
publicKeyPem: string;
|
|
5
|
+
privateKeyPem: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Load or create agentctl's device identity. Uses its own key pair
|
|
9
|
+
* (separate from OpenClaw's identity at ~/.openclaw/identity/device.json).
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadOrCreateDeviceIdentity(filePath?: string): DeviceIdentity;
|
|
2
12
|
export interface OpenClawAdapterOpts {
|
|
3
13
|
baseUrl?: string;
|
|
4
14
|
authToken?: string;
|
|
15
|
+
/** Device identity for scoped access. Auto-created if not provided. */
|
|
16
|
+
deviceIdentity?: DeviceIdentity | null;
|
|
5
17
|
/** Override for testing — replaces the real WebSocket RPC call */
|
|
6
18
|
rpcCall?: RpcCallFn;
|
|
7
19
|
}
|
|
@@ -53,17 +65,63 @@ export interface SessionsPreviewResult {
|
|
|
53
65
|
ts: number;
|
|
54
66
|
previews: SessionsPreviewEntry[];
|
|
55
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the device auth payload string for signing.
|
|
70
|
+
* Format: v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildDeviceAuthPayload(params: {
|
|
73
|
+
deviceId: string;
|
|
74
|
+
clientId: string;
|
|
75
|
+
clientMode: string;
|
|
76
|
+
role: string;
|
|
77
|
+
scopes: string[];
|
|
78
|
+
signedAtMs: number;
|
|
79
|
+
token: string | null;
|
|
80
|
+
nonce: string | null;
|
|
81
|
+
}): string;
|
|
82
|
+
/**
|
|
83
|
+
* Build the full `connect` handshake params sent to the gateway.
|
|
84
|
+
* Includes device auth for scoped access when identity is available.
|
|
85
|
+
* Exported so tests can verify the protocol constants.
|
|
86
|
+
*/
|
|
87
|
+
export declare function buildConnectParams(authToken: string, deviceIdentity?: DeviceIdentity | null, nonce?: string | null): {
|
|
88
|
+
minProtocol: number;
|
|
89
|
+
maxProtocol: number;
|
|
90
|
+
client: {
|
|
91
|
+
id: "cli";
|
|
92
|
+
version: string;
|
|
93
|
+
platform: NodeJS.Platform;
|
|
94
|
+
mode: "cli";
|
|
95
|
+
};
|
|
96
|
+
role: "operator";
|
|
97
|
+
scopes: string[];
|
|
98
|
+
auth: {
|
|
99
|
+
token: string | null;
|
|
100
|
+
};
|
|
101
|
+
device: {
|
|
102
|
+
id: string;
|
|
103
|
+
publicKey: string;
|
|
104
|
+
signature: string;
|
|
105
|
+
signedAt: number;
|
|
106
|
+
nonce: string | undefined;
|
|
107
|
+
} | undefined;
|
|
108
|
+
};
|
|
56
109
|
/**
|
|
57
110
|
* OpenClaw adapter — reads session data from the OpenClaw gateway via
|
|
58
111
|
* its WebSocket RPC protocol. Falls back gracefully when the gateway
|
|
59
112
|
* is unreachable.
|
|
113
|
+
*
|
|
114
|
+
* Uses Ed25519 device auth for scoped access (operator.read).
|
|
115
|
+
* Device identity is auto-created at ~/.agentctl/identity/device.json.
|
|
60
116
|
*/
|
|
61
117
|
export declare class OpenClawAdapter implements AgentAdapter {
|
|
62
118
|
readonly id = "openclaw";
|
|
63
119
|
private readonly baseUrl;
|
|
64
120
|
private readonly authToken;
|
|
121
|
+
private readonly deviceIdentity;
|
|
65
122
|
private readonly rpcCall;
|
|
66
123
|
constructor(opts?: OpenClawAdapterOpts);
|
|
124
|
+
private tryLoadDeviceIdentity;
|
|
67
125
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
68
126
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
69
127
|
status(sessionId: string): Promise<AgentSession>;
|
|
@@ -71,18 +129,11 @@ export declare class OpenClawAdapter implements AgentAdapter {
|
|
|
71
129
|
stop(_sessionId: string, _opts?: StopOpts): Promise<void>;
|
|
72
130
|
resume(sessionId: string, _message: string): Promise<void>;
|
|
73
131
|
events(): AsyncIterable<LifecycleEvent>;
|
|
74
|
-
/**
|
|
75
|
-
* Map a gateway session row to the standard AgentSession interface.
|
|
76
|
-
* OpenClaw sessions with a recent updatedAt are considered "running".
|
|
77
|
-
*/
|
|
78
132
|
private mapRowToSession;
|
|
79
|
-
/**
|
|
80
|
-
* Resolve a sessionId (or prefix) to a gateway session key.
|
|
81
|
-
*/
|
|
82
133
|
private resolveKey;
|
|
83
134
|
/**
|
|
84
|
-
* Real WebSocket RPC call — connects, performs handshake
|
|
85
|
-
* request, reads the response, then disconnects.
|
|
135
|
+
* Real WebSocket RPC call — connects, performs handshake with device auth,
|
|
136
|
+
* sends one request, reads the response, then disconnects.
|
|
86
137
|
*/
|
|
87
138
|
private defaultRpcCall;
|
|
88
139
|
}
|
|
@@ -1,25 +1,206 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import crypto, { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
2
6
|
const DEFAULT_BASE_URL = "http://127.0.0.1:18789";
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
const PKG_VERSION = _require("../../package.json").version;
|
|
9
|
+
// --- Device identity helpers ---
|
|
10
|
+
/** Ed25519 SPKI DER prefix (RFC 8410) — strip to get raw 32-byte public key */
|
|
11
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
12
|
+
function base64UrlEncode(buf) {
|
|
13
|
+
return buf
|
|
14
|
+
.toString("base64")
|
|
15
|
+
.replaceAll("+", "-")
|
|
16
|
+
.replaceAll("/", "_")
|
|
17
|
+
.replace(/=+$/g, "");
|
|
18
|
+
}
|
|
19
|
+
function derivePublicKeyRaw(publicKeyPem) {
|
|
20
|
+
const spki = crypto
|
|
21
|
+
.createPublicKey(publicKeyPem)
|
|
22
|
+
.export({ type: "spki", format: "der" });
|
|
23
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
24
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
25
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
26
|
+
}
|
|
27
|
+
return spki;
|
|
28
|
+
}
|
|
29
|
+
function fingerprintPublicKey(publicKeyPem) {
|
|
30
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
31
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function publicKeyRawBase64Url(publicKeyPem) {
|
|
34
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
35
|
+
}
|
|
36
|
+
function signPayload(privateKeyPem, payload) {
|
|
37
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
38
|
+
return base64UrlEncode(Buffer.from(crypto.sign(null, Buffer.from(payload, "utf8"), key)));
|
|
39
|
+
}
|
|
40
|
+
function generateDeviceIdentity() {
|
|
41
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
42
|
+
const publicKeyPem = publicKey
|
|
43
|
+
.export({ type: "spki", format: "pem" })
|
|
44
|
+
.toString();
|
|
45
|
+
const privateKeyPem = privateKey
|
|
46
|
+
.export({ type: "pkcs8", format: "pem" })
|
|
47
|
+
.toString();
|
|
48
|
+
return {
|
|
49
|
+
deviceId: fingerprintPublicKey(publicKeyPem),
|
|
50
|
+
publicKeyPem,
|
|
51
|
+
privateKeyPem,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Default identity path: ~/.agentctl/identity/device.json */
|
|
55
|
+
function resolveDefaultIdentityPath() {
|
|
56
|
+
return path.join(os.homedir(), ".agentctl", "identity", "device.json");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Load or create agentctl's device identity. Uses its own key pair
|
|
60
|
+
* (separate from OpenClaw's identity at ~/.openclaw/identity/device.json).
|
|
61
|
+
*/
|
|
62
|
+
export function loadOrCreateDeviceIdentity(filePath = resolveDefaultIdentityPath()) {
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(filePath)) {
|
|
65
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
if (parsed?.version === 1 &&
|
|
68
|
+
typeof parsed.deviceId === "string" &&
|
|
69
|
+
typeof parsed.publicKeyPem === "string" &&
|
|
70
|
+
typeof parsed.privateKeyPem === "string") {
|
|
71
|
+
// Re-derive deviceId from public key in case it drifted
|
|
72
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
73
|
+
return {
|
|
74
|
+
deviceId: derivedId,
|
|
75
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
76
|
+
privateKeyPem: parsed.privateKeyPem,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Fall through to generate new identity
|
|
83
|
+
}
|
|
84
|
+
const identity = generateDeviceIdentity();
|
|
85
|
+
const dir = path.dirname(filePath);
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
const stored = {
|
|
88
|
+
version: 1,
|
|
89
|
+
deviceId: identity.deviceId,
|
|
90
|
+
publicKeyPem: identity.publicKeyPem,
|
|
91
|
+
privateKeyPem: identity.privateKeyPem,
|
|
92
|
+
createdAtMs: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}\n`, {
|
|
95
|
+
mode: 0o600,
|
|
96
|
+
});
|
|
97
|
+
return identity;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the device auth payload string for signing.
|
|
101
|
+
* Format: v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce
|
|
102
|
+
*/
|
|
103
|
+
export function buildDeviceAuthPayload(params) {
|
|
104
|
+
const version = params.nonce ? "v2" : "v1";
|
|
105
|
+
const base = [
|
|
106
|
+
version,
|
|
107
|
+
params.deviceId,
|
|
108
|
+
params.clientId,
|
|
109
|
+
params.clientMode,
|
|
110
|
+
params.role,
|
|
111
|
+
params.scopes.join(","),
|
|
112
|
+
String(params.signedAtMs),
|
|
113
|
+
params.token ?? "",
|
|
114
|
+
];
|
|
115
|
+
if (version === "v2")
|
|
116
|
+
base.push(params.nonce ?? "");
|
|
117
|
+
return base.join("|");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the full `connect` handshake params sent to the gateway.
|
|
121
|
+
* Includes device auth for scoped access when identity is available.
|
|
122
|
+
* Exported so tests can verify the protocol constants.
|
|
123
|
+
*/
|
|
124
|
+
export function buildConnectParams(authToken, deviceIdentity, nonce) {
|
|
125
|
+
const scopes = ["operator.read"];
|
|
126
|
+
const signedAtMs = Date.now();
|
|
127
|
+
const device = deviceIdentity
|
|
128
|
+
? (() => {
|
|
129
|
+
const payload = buildDeviceAuthPayload({
|
|
130
|
+
deviceId: deviceIdentity.deviceId,
|
|
131
|
+
clientId: "cli",
|
|
132
|
+
clientMode: "cli",
|
|
133
|
+
role: "operator",
|
|
134
|
+
scopes,
|
|
135
|
+
signedAtMs,
|
|
136
|
+
token: authToken || null,
|
|
137
|
+
nonce: nonce ?? null,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
id: deviceIdentity.deviceId,
|
|
141
|
+
publicKey: publicKeyRawBase64Url(deviceIdentity.publicKeyPem),
|
|
142
|
+
signature: signPayload(deviceIdentity.privateKeyPem, payload),
|
|
143
|
+
signedAt: signedAtMs,
|
|
144
|
+
nonce: nonce ?? undefined,
|
|
145
|
+
};
|
|
146
|
+
})()
|
|
147
|
+
: undefined;
|
|
148
|
+
return {
|
|
149
|
+
minProtocol: 1,
|
|
150
|
+
maxProtocol: 3,
|
|
151
|
+
client: {
|
|
152
|
+
id: "cli",
|
|
153
|
+
version: PKG_VERSION,
|
|
154
|
+
platform: process.platform,
|
|
155
|
+
mode: "cli",
|
|
156
|
+
},
|
|
157
|
+
role: "operator",
|
|
158
|
+
scopes,
|
|
159
|
+
auth: { token: authToken || null },
|
|
160
|
+
device,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
3
163
|
/**
|
|
4
164
|
* OpenClaw adapter — reads session data from the OpenClaw gateway via
|
|
5
165
|
* its WebSocket RPC protocol. Falls back gracefully when the gateway
|
|
6
166
|
* is unreachable.
|
|
167
|
+
*
|
|
168
|
+
* Uses Ed25519 device auth for scoped access (operator.read).
|
|
169
|
+
* Device identity is auto-created at ~/.agentctl/identity/device.json.
|
|
7
170
|
*/
|
|
8
171
|
export class OpenClawAdapter {
|
|
9
172
|
id = "openclaw";
|
|
10
173
|
baseUrl;
|
|
11
174
|
authToken;
|
|
175
|
+
deviceIdentity;
|
|
12
176
|
rpcCall;
|
|
13
177
|
constructor(opts) {
|
|
14
178
|
this.baseUrl = opts?.baseUrl || DEFAULT_BASE_URL;
|
|
15
179
|
this.authToken =
|
|
16
|
-
opts?.authToken ||
|
|
180
|
+
opts?.authToken ||
|
|
181
|
+
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
|
182
|
+
process.env.OPENCLAW_WEBHOOK_TOKEN ||
|
|
183
|
+
"";
|
|
184
|
+
// Device identity: explicit null disables it (for tests), undefined = auto-create
|
|
185
|
+
this.deviceIdentity =
|
|
186
|
+
opts?.deviceIdentity === null
|
|
187
|
+
? null
|
|
188
|
+
: (opts?.deviceIdentity ?? this.tryLoadDeviceIdentity());
|
|
17
189
|
this.rpcCall = opts?.rpcCall || this.defaultRpcCall.bind(this);
|
|
18
190
|
}
|
|
191
|
+
tryLoadDeviceIdentity() {
|
|
192
|
+
try {
|
|
193
|
+
return loadOrCreateDeviceIdentity();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Don't break adapter construction if identity can't be created
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
19
200
|
async list(opts) {
|
|
20
201
|
if (!this.authToken) {
|
|
21
|
-
console.warn("Warning:
|
|
22
|
-
"Set
|
|
202
|
+
console.warn("Warning: OPENCLAW_GATEWAY_TOKEN is not set — OpenClaw adapter cannot authenticate. " +
|
|
203
|
+
"Set OPENCLAW_GATEWAY_TOKEN (or OPENCLAW_WEBHOOK_TOKEN) to connect to the gateway.");
|
|
23
204
|
return [];
|
|
24
205
|
}
|
|
25
206
|
let result;
|
|
@@ -33,7 +214,7 @@ export class OpenClawAdapter {
|
|
|
33
214
|
const msg = err?.message || "unknown error";
|
|
34
215
|
if (msg.includes("auth") || msg.includes("Auth")) {
|
|
35
216
|
console.warn(`Warning: OpenClaw gateway authentication failed: ${msg}. ` +
|
|
36
|
-
"Check that OPENCLAW_WEBHOOK_TOKEN is valid.");
|
|
217
|
+
"Check that OPENCLAW_GATEWAY_TOKEN (or OPENCLAW_WEBHOOK_TOKEN) is valid.");
|
|
37
218
|
}
|
|
38
219
|
else {
|
|
39
220
|
console.warn(`Warning: OpenClaw gateway unreachable (${msg}). ` +
|
|
@@ -81,7 +262,7 @@ export class OpenClawAdapter {
|
|
|
81
262
|
}
|
|
82
263
|
async status(sessionId) {
|
|
83
264
|
if (!this.authToken) {
|
|
84
|
-
throw new Error("
|
|
265
|
+
throw new Error("OPENCLAW_GATEWAY_TOKEN is not set — cannot connect to OpenClaw gateway");
|
|
85
266
|
}
|
|
86
267
|
let result;
|
|
87
268
|
try {
|
|
@@ -108,14 +289,10 @@ export class OpenClawAdapter {
|
|
|
108
289
|
throw new Error("OpenClaw sessions cannot be stopped via agentctl");
|
|
109
290
|
}
|
|
110
291
|
async resume(sessionId, _message) {
|
|
111
|
-
// OpenClaw sessions receive messages through their configured channels,
|
|
112
|
-
// not through a direct CLI interface.
|
|
113
292
|
throw new Error(`Cannot resume OpenClaw session ${sessionId} — use the gateway UI or configured channel`);
|
|
114
293
|
}
|
|
115
294
|
async *events() {
|
|
116
|
-
// Poll-based diffing (same pattern as claude-code)
|
|
117
295
|
let knownSessions = new Map();
|
|
118
|
-
// Initial snapshot
|
|
119
296
|
const initial = await this.list({ all: true });
|
|
120
297
|
for (const s of initial) {
|
|
121
298
|
knownSessions.set(s.id, s);
|
|
@@ -164,15 +341,10 @@ export class OpenClawAdapter {
|
|
|
164
341
|
}
|
|
165
342
|
}
|
|
166
343
|
// --- Private helpers ---
|
|
167
|
-
/**
|
|
168
|
-
* Map a gateway session row to the standard AgentSession interface.
|
|
169
|
-
* OpenClaw sessions with a recent updatedAt are considered "running".
|
|
170
|
-
*/
|
|
171
344
|
mapRowToSession(row, defaults) {
|
|
172
345
|
const now = Date.now();
|
|
173
346
|
const updatedAt = row.updatedAt ?? 0;
|
|
174
347
|
const ageMs = now - updatedAt;
|
|
175
|
-
// Consider "running" if updated in the last 5 minutes
|
|
176
348
|
const isActive = updatedAt > 0 && ageMs < 5 * 60 * 1000;
|
|
177
349
|
const model = row.model || defaults.model || undefined;
|
|
178
350
|
const input = row.inputTokens ?? 0;
|
|
@@ -196,12 +368,9 @@ export class OpenClawAdapter {
|
|
|
196
368
|
},
|
|
197
369
|
};
|
|
198
370
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Resolve a sessionId (or prefix) to a gateway session key.
|
|
201
|
-
*/
|
|
202
371
|
async resolveKey(sessionId) {
|
|
203
372
|
if (!this.authToken) {
|
|
204
|
-
throw new Error("
|
|
373
|
+
throw new Error("OPENCLAW_GATEWAY_TOKEN is not set — cannot connect to OpenClaw gateway");
|
|
205
374
|
}
|
|
206
375
|
let result;
|
|
207
376
|
try {
|
|
@@ -219,13 +388,11 @@ export class OpenClawAdapter {
|
|
|
219
388
|
return row?.key ?? null;
|
|
220
389
|
}
|
|
221
390
|
/**
|
|
222
|
-
* Real WebSocket RPC call — connects, performs handshake
|
|
223
|
-
* request, reads the response, then disconnects.
|
|
391
|
+
* Real WebSocket RPC call — connects, performs handshake with device auth,
|
|
392
|
+
* sends one request, reads the response, then disconnects.
|
|
224
393
|
*/
|
|
225
394
|
async defaultRpcCall(method, params) {
|
|
226
|
-
// Dynamic import so tests can inject a mock without loading ws
|
|
227
395
|
const { WebSocket } = await import("ws").catch(() => {
|
|
228
|
-
// Fall back to globalThis.WebSocket (available in Node 22+)
|
|
229
396
|
return { WebSocket: globalThis.WebSocket };
|
|
230
397
|
});
|
|
231
398
|
const wsUrl = this.baseUrl.replace(/^http/, "ws");
|
|
@@ -244,25 +411,16 @@ export class OpenClawAdapter {
|
|
|
244
411
|
try {
|
|
245
412
|
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
246
413
|
const frame = JSON.parse(raw);
|
|
247
|
-
// Step 1: Receive challenge, send connect
|
|
414
|
+
// Step 1: Receive challenge (with nonce), send connect with device auth
|
|
248
415
|
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
416
|
+
const nonce = frame.payload && typeof frame.payload.nonce === "string"
|
|
417
|
+
? frame.payload.nonce
|
|
418
|
+
: null;
|
|
249
419
|
ws.send(JSON.stringify({
|
|
250
420
|
type: "req",
|
|
251
421
|
id: randomUUID(),
|
|
252
422
|
method: "connect",
|
|
253
|
-
params:
|
|
254
|
-
minProtocol: 1,
|
|
255
|
-
maxProtocol: 1,
|
|
256
|
-
client: {
|
|
257
|
-
id: "agentctl",
|
|
258
|
-
version: "0.1.0",
|
|
259
|
-
platform: process.platform,
|
|
260
|
-
mode: "cli",
|
|
261
|
-
},
|
|
262
|
-
role: "operator",
|
|
263
|
-
scopes: ["operator.read"],
|
|
264
|
-
auth: { token: this.authToken || null },
|
|
265
|
-
},
|
|
423
|
+
params: buildConnectParams(this.authToken, this.deviceIdentity, nonce),
|
|
266
424
|
}));
|
|
267
425
|
return;
|
|
268
426
|
}
|
|
@@ -306,7 +464,6 @@ export class OpenClawAdapter {
|
|
|
306
464
|
};
|
|
307
465
|
ws.onclose = () => {
|
|
308
466
|
clearTimeout(timeout);
|
|
309
|
-
// Only reject if we haven't resolved yet
|
|
310
467
|
};
|
|
311
468
|
});
|
|
312
469
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
|
+
export interface PidInfo {
|
|
3
|
+
pid: number;
|
|
4
|
+
cwd: string;
|
|
5
|
+
args: string;
|
|
6
|
+
/** Process start time from `ps -p <pid> -o lstart=`, used to detect PID recycling */
|
|
7
|
+
startTime?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Metadata persisted by launch() so status checks survive wrapper exit */
|
|
10
|
+
export interface LaunchedSessionMeta {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
pid: number;
|
|
13
|
+
/** Process start time from `ps -p <pid> -o lstart=` for PID recycling detection */
|
|
14
|
+
startTime?: string;
|
|
15
|
+
/** The PID of the wrapper (agentctl launch) — may differ from `pid` (opencode process) */
|
|
16
|
+
wrapperPid?: number;
|
|
17
|
+
cwd: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
prompt?: string;
|
|
20
|
+
launchedAt: string;
|
|
21
|
+
}
|
|
22
|
+
/** Shape of an OpenCode session JSON file */
|
|
23
|
+
export interface OpenCodeSessionFile {
|
|
24
|
+
id: string;
|
|
25
|
+
slug?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
projectID?: string;
|
|
28
|
+
directory?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
time?: {
|
|
31
|
+
created?: number | string;
|
|
32
|
+
updated?: number | string;
|
|
33
|
+
};
|
|
34
|
+
summary?: {
|
|
35
|
+
additions?: number;
|
|
36
|
+
deletions?: number;
|
|
37
|
+
files?: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Shape of an OpenCode message JSON file */
|
|
41
|
+
export interface OpenCodeMessageFile {
|
|
42
|
+
id: string;
|
|
43
|
+
sessionID?: string;
|
|
44
|
+
role?: "user" | "assistant";
|
|
45
|
+
time?: {
|
|
46
|
+
created?: number | string;
|
|
47
|
+
completed?: number | string;
|
|
48
|
+
};
|
|
49
|
+
agent?: string;
|
|
50
|
+
model?: {
|
|
51
|
+
providerID?: string;
|
|
52
|
+
modelID?: string;
|
|
53
|
+
};
|
|
54
|
+
tokens?: {
|
|
55
|
+
input?: number;
|
|
56
|
+
output?: number;
|
|
57
|
+
reasoning?: number;
|
|
58
|
+
};
|
|
59
|
+
cache?: {
|
|
60
|
+
read?: number;
|
|
61
|
+
write?: number;
|
|
62
|
+
};
|
|
63
|
+
cost?: number;
|
|
64
|
+
finish?: string;
|
|
65
|
+
error?: {
|
|
66
|
+
name?: string;
|
|
67
|
+
data?: {
|
|
68
|
+
message?: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
modelID?: string;
|
|
72
|
+
providerID?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface OpenCodeAdapterOpts {
|
|
75
|
+
storageDir?: string;
|
|
76
|
+
sessionsMetaDir?: string;
|
|
77
|
+
getPids?: () => Promise<Map<number, PidInfo>>;
|
|
78
|
+
/** Override PID liveness check for testing (default: process.kill(pid, 0)) */
|
|
79
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compute the project hash matching OpenCode's approach: SHA1 of the directory path.
|
|
83
|
+
*/
|
|
84
|
+
export declare function computeProjectHash(directory: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* OpenCode adapter — reads session data from ~/.local/share/opencode/storage/
|
|
87
|
+
* and cross-references with running opencode processes.
|
|
88
|
+
*/
|
|
89
|
+
export declare class OpenCodeAdapter implements AgentAdapter {
|
|
90
|
+
readonly id = "opencode";
|
|
91
|
+
private readonly storageDir;
|
|
92
|
+
private readonly sessionDir;
|
|
93
|
+
private readonly messageDir;
|
|
94
|
+
private readonly sessionsMetaDir;
|
|
95
|
+
private readonly getPids;
|
|
96
|
+
private readonly isProcessAlive;
|
|
97
|
+
constructor(opts?: OpenCodeAdapterOpts);
|
|
98
|
+
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
99
|
+
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
100
|
+
status(sessionId: string): Promise<AgentSession>;
|
|
101
|
+
launch(opts: LaunchOpts): Promise<AgentSession>;
|
|
102
|
+
stop(sessionId: string, opts?: StopOpts): Promise<void>;
|
|
103
|
+
resume(sessionId: string, message: string): Promise<void>;
|
|
104
|
+
events(): AsyncIterable<LifecycleEvent>;
|
|
105
|
+
/**
|
|
106
|
+
* Read all session JSON files for a project directory.
|
|
107
|
+
*/
|
|
108
|
+
private getSessionFilesForProject;
|
|
109
|
+
/**
|
|
110
|
+
* Build an AgentSession from an OpenCode session file.
|
|
111
|
+
*/
|
|
112
|
+
private buildSession;
|
|
113
|
+
/**
|
|
114
|
+
* Check whether a session is currently running by cross-referencing PIDs.
|
|
115
|
+
*/
|
|
116
|
+
private isSessionRunning;
|
|
117
|
+
/**
|
|
118
|
+
* Check whether a process plausibly belongs to a session by verifying
|
|
119
|
+
* the process started at or after the session's creation time.
|
|
120
|
+
*/
|
|
121
|
+
private processStartedAfterSession;
|
|
122
|
+
private findMatchingPid;
|
|
123
|
+
/**
|
|
124
|
+
* Read all messages for a session and aggregate stats.
|
|
125
|
+
*/
|
|
126
|
+
private aggregateMessageStats;
|
|
127
|
+
/**
|
|
128
|
+
* Read all message files for a session, sorted by time.
|
|
129
|
+
*/
|
|
130
|
+
private readMessages;
|
|
131
|
+
/**
|
|
132
|
+
* Read message content parts from storage/part/<messageId>/
|
|
133
|
+
*/
|
|
134
|
+
private readMessageParts;
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a session ID (supports prefix matching).
|
|
137
|
+
*/
|
|
138
|
+
private resolveSessionId;
|
|
139
|
+
private findPidForSession;
|
|
140
|
+
writeSessionMeta(meta: Omit<LaunchedSessionMeta, "startTime">): Promise<void>;
|
|
141
|
+
readSessionMeta(sessionId: string): Promise<LaunchedSessionMeta | null>;
|
|
142
|
+
private deleteSessionMeta;
|
|
143
|
+
}
|