libfx 0.0.13 → 0.0.14-dev.1561.g1a070af32cdf
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 +454 -486
- package/browser.js +18 -4
- package/chatgpt-sign-in.js +141 -0
- package/chatgpt.js +69 -0
- package/custom-provider.js +123 -0
- package/durable/local.mjs +190 -0
- package/durable/vercel.mjs +268 -0
- package/durable/world.mjs +1 -0
- package/durable.js +2752 -0
- package/fetch-cleanup.js +12 -0
- package/fx-core.wasm +0 -0
- package/fx-harness.js +220 -0
- package/fx-sdk.js +1302 -116
- package/fx-term.wasm +0 -0
- package/grok-sign-in.js +145 -0
- package/grok.js +108 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +14 -2
- package/node.cjs +4508 -213
- package/node.js +195 -47
- package/oauth.js +64 -0
- package/package.json +66 -13
- package/subscription.js +253 -0
- package/tool-observation.js +185 -0
- package/types/browser.d.ts +146 -0
- package/types/chatgpt.d.ts +58 -0
- package/types/durable-local.d.ts +18 -0
- package/types/durable-vercel.d.ts +22 -0
- package/types/durable-world.d.ts +95 -0
- package/types/grok.d.ts +61 -0
- package/types/libfx.d.cts +1061 -0
- package/types/mcp.d.ts +46 -0
- package/types/node.d.cts +165 -0
- package/types/node.d.ts +3 -0
- package/types/skills-node.d.ts +25 -0
- package/types/skills.d.ts +28 -0
- package/types/wasm.d.ts +106 -0
package/subscription.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Subscription credentials for libfx agents: Codex on a ChatGPT plan, Grok on
|
|
2
|
+
// a Grok plan. The engine sends these providers' requests without credentials;
|
|
3
|
+
// this module adds them at the fetch edge, from the host's `credentials`
|
|
4
|
+
// callback, refreshing near expiry and once after a rejection. Credentials
|
|
5
|
+
// never enter the engine or a session's saved records, and are masked in the
|
|
6
|
+
// body of a failed response.
|
|
7
|
+
//
|
|
8
|
+
// A provider spec supplies what differs: which requests it authorizes, how its
|
|
9
|
+
// credentials are checked and refreshed, and which headers carry them.
|
|
10
|
+
//
|
|
11
|
+
// TLA+ model: `tests/tla/sdk/ProviderAuth.tla`.
|
|
12
|
+
|
|
13
|
+
const refreshSkewMs = 5 * 60 * 1000;
|
|
14
|
+
const maxRememberedCredentials = 256;
|
|
15
|
+
const maxTokenBytes = 64 * 1024;
|
|
16
|
+
const maxErrorBodyBytes = 64 * 1024;
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
|
|
19
|
+
/** A non-empty, bounded credential field, or undefined when optional and absent. */
|
|
20
|
+
export function tokenString(value, name, required, label) {
|
|
21
|
+
if (value === undefined && !required) return undefined;
|
|
22
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
23
|
+
throw new TypeError(`${label} credentials ${name} must be a non-empty string`);
|
|
24
|
+
}
|
|
25
|
+
if (encoder.encode(value).length > maxTokenBytes) {
|
|
26
|
+
throw new RangeError(`${label} credentials ${name} exceeds ${maxTokenBytes} bytes`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The claims of a JWT, or null when it is not one. Does not verify it. */
|
|
32
|
+
export function jwtClaims(token) {
|
|
33
|
+
const part = typeof token === "string" ? token.split(".")[1] : undefined;
|
|
34
|
+
if (!part) return null;
|
|
35
|
+
try {
|
|
36
|
+
const base64 = part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=");
|
|
37
|
+
const binary = atob(base64);
|
|
38
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
39
|
+
const claims = JSON.parse(new TextDecoder().decode(bytes));
|
|
40
|
+
return claims && typeof claims === "object" ? claims : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates credentials from the host. `accountId` and `expiresAt` default to
|
|
48
|
+
* the access token's claims, `accountClaim(claims)` picking the account.
|
|
49
|
+
* Returns a new object.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeCredentials(value, { label, accountClaim }) {
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
throw new TypeError("credentials() must return { accessToken, refreshToken?, accountId?, expiresAt? }");
|
|
54
|
+
}
|
|
55
|
+
const accessToken = tokenString(value.accessToken, "accessToken", true, label);
|
|
56
|
+
const refreshToken = tokenString(value.refreshToken, "refreshToken", false, label);
|
|
57
|
+
const claims = jwtClaims(accessToken);
|
|
58
|
+
const accountId = value.accountId === undefined ? accountClaim(claims) : tokenString(value.accountId, "accountId", true, label);
|
|
59
|
+
if (typeof accountId !== "string" || accountId.length === 0) {
|
|
60
|
+
throw new TypeError(`${label} credentials need an accountId; it was not in the access token`);
|
|
61
|
+
}
|
|
62
|
+
let expiresAt = value.expiresAt;
|
|
63
|
+
if (expiresAt === undefined && Number.isFinite(claims?.exp)) expiresAt = claims.exp * 1000;
|
|
64
|
+
if (expiresAt !== undefined && !Number.isFinite(expiresAt)) {
|
|
65
|
+
throw new TypeError(`${label} credentials expiresAt must be milliseconds since the epoch`);
|
|
66
|
+
}
|
|
67
|
+
return { accessToken, ...(refreshToken === undefined ? {} : { refreshToken }), accountId, ...(expiresAt === undefined ? {} : { expiresAt }) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A failed response whose body cannot repeat the credentials its request
|
|
72
|
+
* carried. Some servers quote the bearer token back in an error, and the
|
|
73
|
+
* engine shows and saves error details, so this edge, the only code that knows
|
|
74
|
+
* the token, masks it the way fx masks error details from its own providers.
|
|
75
|
+
* JSON is normalized first, so an escaped copy cannot survive.
|
|
76
|
+
*/
|
|
77
|
+
async function withoutCredentials(response, credentials) {
|
|
78
|
+
if (response.status < 400) return response;
|
|
79
|
+
const raw = await boundedText(response, maxErrorBodyBytes);
|
|
80
|
+
let body = raw ?? "Provider error details exceeded the local limit";
|
|
81
|
+
if (raw !== null && /^\s*[[{"]/.test(raw)) {
|
|
82
|
+
try {
|
|
83
|
+
body = JSON.stringify(JSON.parse(raw));
|
|
84
|
+
} catch {
|
|
85
|
+
body = "Provider error details could not be decoded";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const secret of [credentials.accessToken, credentials.refreshToken]) {
|
|
89
|
+
if (!secret) continue;
|
|
90
|
+
for (const form of new Set([secret, JSON.stringify(secret).slice(1, -1)])) body = body.split(form).join("*".repeat(form.length));
|
|
91
|
+
}
|
|
92
|
+
const headers = new Headers(response.headers);
|
|
93
|
+
headers.delete("content-length");
|
|
94
|
+
headers.delete("content-encoding");
|
|
95
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The body as text, or null once it exceeds `limit` bytes. */
|
|
99
|
+
async function boundedText(response, limit) {
|
|
100
|
+
if (!response.body) return "";
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let size = 0;
|
|
104
|
+
let text = "";
|
|
105
|
+
for (;;) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done) return text + decoder.decode();
|
|
108
|
+
size += value.byteLength;
|
|
109
|
+
if (size > limit) {
|
|
110
|
+
await reader.cancel().catch(() => {});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
text += decoder.decode(value, { stream: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** True when the credentials can and should be refreshed before use. */
|
|
118
|
+
export function needsRefresh(credentials, nowMs) {
|
|
119
|
+
return credentials.refreshToken !== undefined &&
|
|
120
|
+
credentials.expiresAt !== undefined &&
|
|
121
|
+
credentials.expiresAt - refreshSkewMs <= nowMs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The engine option that carries an agent's `createCredentialMemory()`. */
|
|
125
|
+
export const credentialMemory = Symbol.for("libfx.credentialMemory");
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Refresh state for `createSubscriptionAuthorizer()`. An agent gives the same
|
|
129
|
+
* memory to every session's engine, so its sessions refresh an account once.
|
|
130
|
+
*/
|
|
131
|
+
export function createCredentialMemory() {
|
|
132
|
+
return { descendants: new Map(), refreshing: new Map(), saved: new WeakMap() };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The fetch edge for subscription requests. `credentials({ provider, sessionId })`
|
|
137
|
+
* supplies credentials; `onCredentials(next, { provider, sessionId })` receives
|
|
138
|
+
* refreshed ones once for each session that uses them, so the host can save them.
|
|
139
|
+
* `refreshLock({ provider, sessionId }, refresh)` runs `refresh` while the host
|
|
140
|
+
* holds a lock on the account, for a host with several servers.
|
|
141
|
+
*
|
|
142
|
+
* Each spec: `{ provider, matches(endpoint), sessionId(headers), normalize(value),
|
|
143
|
+
* refresh(credentials, { fetch, signal }), authorize(headers, endpoint, credentials) }`.
|
|
144
|
+
*/
|
|
145
|
+
export function createSubscriptionAuthorizer(specs, { credentials, onCredentials, refreshLock, fetch, now = Date.now, memory = createCredentialMemory() }) {
|
|
146
|
+
if (typeof credentials !== "function") throw new TypeError("a subscription provider needs a credentials() function");
|
|
147
|
+
if (onCredentials !== undefined && typeof onCredentials !== "function") {
|
|
148
|
+
throw new TypeError("onCredentials must be a function");
|
|
149
|
+
}
|
|
150
|
+
if (refreshLock !== undefined && typeof refreshLock !== "function") {
|
|
151
|
+
throw new TypeError("refreshLock must be a function");
|
|
152
|
+
}
|
|
153
|
+
// Each access token the host supplied maps to the newest credentials refreshed
|
|
154
|
+
// from it, so a host that has not saved a rotation yet never spends the old
|
|
155
|
+
// refresh token twice. Engines that share `memory` share this.
|
|
156
|
+
const { descendants, refreshing, saved } = memory;
|
|
157
|
+
const keyOf = (spec, token) => `${spec.provider}\u0000${token}`;
|
|
158
|
+
|
|
159
|
+
const remember = (spec, supplied, next) => {
|
|
160
|
+
const key = keyOf(spec, supplied.accessToken);
|
|
161
|
+
descendants.delete(key);
|
|
162
|
+
descendants.set(key, next);
|
|
163
|
+
if (descendants.size > maxRememberedCredentials) descendants.delete(descendants.keys().next().value);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// A host that keeps credentials per session must also save a rotation that
|
|
167
|
+
// another session's request made.
|
|
168
|
+
const savedBy = (next) => {
|
|
169
|
+
let sessions = saved.get(next);
|
|
170
|
+
if (!sessions) saved.set(next, (sessions = new Set()));
|
|
171
|
+
return sessions;
|
|
172
|
+
};
|
|
173
|
+
const save = async (spec, next, sessionId) => {
|
|
174
|
+
if (!onCredentials) return;
|
|
175
|
+
const sessions = savedBy(next);
|
|
176
|
+
if (sessions.has(sessionId)) return;
|
|
177
|
+
sessions.add(sessionId);
|
|
178
|
+
await onCredentials(next, { provider: spec.provider, sessionId });
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const redeem = async (spec, supplied, current, sessionId, signal) => {
|
|
182
|
+
if (refreshLock) {
|
|
183
|
+
// Another server may have refreshed and saved them while this one waited.
|
|
184
|
+
const latest = spec.normalize(await credentials({ provider: spec.provider, sessionId }));
|
|
185
|
+
if (latest.accessToken !== current.accessToken && latest.accessToken !== supplied.accessToken && !needsRefresh(latest, now())) {
|
|
186
|
+
remember(spec, supplied, latest);
|
|
187
|
+
savedBy(latest).add(sessionId);
|
|
188
|
+
return latest;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const next = await spec.refresh(current, { fetch, signal });
|
|
192
|
+
remember(spec, supplied, next);
|
|
193
|
+
await save(spec, next, sessionId);
|
|
194
|
+
return next;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// With `refreshLock`, the host holds a lock on the account while the refresh
|
|
198
|
+
// runs, so its servers refresh one at a time.
|
|
199
|
+
const locked = async (spec, supplied, current, sessionId, signal) => {
|
|
200
|
+
let next;
|
|
201
|
+
await refreshLock({ provider: spec.provider, sessionId }, async () => {
|
|
202
|
+
next = await redeem(spec, supplied, current, sessionId, signal);
|
|
203
|
+
});
|
|
204
|
+
if (next === undefined) throw new Error("refreshLock must run and await the refresh it is given");
|
|
205
|
+
return next;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const refresh = async (spec, supplied, current, sessionId, signal) => {
|
|
209
|
+
const key = keyOf(spec, current.refreshToken);
|
|
210
|
+
let task = refreshing.get(key);
|
|
211
|
+
if (!task) {
|
|
212
|
+
task = (refreshLock ? locked : redeem)(spec, supplied, current, sessionId, signal).finally(() => refreshing.delete(key));
|
|
213
|
+
refreshing.set(key, task);
|
|
214
|
+
}
|
|
215
|
+
const next = await task;
|
|
216
|
+
if (sessionId !== undefined) await save(spec, next, sessionId);
|
|
217
|
+
return next;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const current = async (spec, sessionId, signal) => {
|
|
221
|
+
const supplied = spec.normalize(await credentials({ provider: spec.provider, sessionId }));
|
|
222
|
+
const best = descendants.get(keyOf(spec, supplied.accessToken)) ?? supplied;
|
|
223
|
+
const used = needsRefresh(best, now()) ? await refresh(spec, supplied, best, sessionId, signal) : best;
|
|
224
|
+
if (used !== supplied && sessionId !== undefined) await save(spec, used, sessionId);
|
|
225
|
+
return { supplied, used };
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const withCredentials = (spec, endpoint, init, used) => {
|
|
229
|
+
const headers = new Headers(init.headers);
|
|
230
|
+
spec.authorize(headers, endpoint, used);
|
|
231
|
+
return { ...init, headers };
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
/** The spec that authorizes requests to `endpoint` (origin and path), if any. */
|
|
236
|
+
specFor: (endpoint) => specs.find((spec) => spec.matches(endpoint)),
|
|
237
|
+
/** Sends one request with credentials, refreshing once after a 401. */
|
|
238
|
+
async fetch(spec, endpoint, hostFetch, input, init = {}) {
|
|
239
|
+
const sessionId = spec.sessionId(new Headers(init.headers)) || undefined;
|
|
240
|
+
const { used } = await current(spec, sessionId, init.signal);
|
|
241
|
+
const response = await hostFetch(input, withCredentials(spec, endpoint, init, used));
|
|
242
|
+
if (response.status !== 401 || used.refreshToken === undefined) return withoutCredentials(response, used);
|
|
243
|
+
await response.body?.cancel().catch(() => {});
|
|
244
|
+
// Another request may have replaced these credentials while this one was
|
|
245
|
+
// out. Only a rejection of the newest ones spends a refresh token.
|
|
246
|
+
const latest = await current(spec, sessionId, init.signal);
|
|
247
|
+
const next = latest.used.accessToken !== used.accessToken
|
|
248
|
+
? latest.used
|
|
249
|
+
: await refresh(spec, latest.supplied, used, sessionId, init.signal);
|
|
250
|
+
return withoutCredentials(await hostFetch(input, withCredentials(spec, endpoint, init, next)), next);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Observations of host-owned effects. Helpers call the effect exactly once,
|
|
2
|
+
// preserve its value/error, and never await an instrumentation callback.
|
|
3
|
+
export function toolObservation(scope, callId, hooks, emit, signal) {
|
|
4
|
+
const report = (error) => {
|
|
5
|
+
try {
|
|
6
|
+
Promise.resolve(hooks.onError?.(error)).catch(() => {});
|
|
7
|
+
} catch {}
|
|
8
|
+
};
|
|
9
|
+
let next = 0;
|
|
10
|
+
const id = () => `host:${scope.executionId}:${callId}:${++next}`;
|
|
11
|
+
const publish = (type, data, content = {}) =>
|
|
12
|
+
emit({
|
|
13
|
+
...scope,
|
|
14
|
+
type,
|
|
15
|
+
data,
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
contentCaptured: hooks.captureContent === true,
|
|
18
|
+
contentTruncated: content.truncated === true,
|
|
19
|
+
});
|
|
20
|
+
const capture = () => {
|
|
21
|
+
let remaining = 64 * 1024;
|
|
22
|
+
return {
|
|
23
|
+
truncated: false,
|
|
24
|
+
text(value) {
|
|
25
|
+
if (hooks.captureContent !== true || value == null) return undefined;
|
|
26
|
+
value = String(value);
|
|
27
|
+
let prefix = value.slice(0, remaining);
|
|
28
|
+
if (prefix.length && /[\uD800-\uDBFF]/.test(prefix.at(-1))) prefix = prefix.slice(0, -1);
|
|
29
|
+
const bytes = new TextEncoder().encode(prefix);
|
|
30
|
+
let end = Math.min(bytes.length, remaining);
|
|
31
|
+
while (end > 0 && end < bytes.length && (bytes[end] & 0xc0) === 0x80) end--;
|
|
32
|
+
remaining -= end;
|
|
33
|
+
this.truncated ||= prefix.length < value.length || end < bytes.length;
|
|
34
|
+
return new TextDecoder().decode(bytes.subarray(0, end));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
function terminal(end) {
|
|
39
|
+
let ended = false;
|
|
40
|
+
const finish = (...args) => {
|
|
41
|
+
if (ended) return;
|
|
42
|
+
ended = true;
|
|
43
|
+
signal.removeEventListener("abort", abort);
|
|
44
|
+
try {
|
|
45
|
+
end(...args);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
report(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const abort = () => finish("cancelled", undefined);
|
|
51
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
52
|
+
if (signal.aborted) abort();
|
|
53
|
+
return finish;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
async wait(request, run) {
|
|
57
|
+
if (
|
|
58
|
+
!request ||
|
|
59
|
+
!["approval", "question", "mcp_form", "mcp_url"].includes(request.kind) ||
|
|
60
|
+
typeof run !== "function"
|
|
61
|
+
)
|
|
62
|
+
throw new TypeError("wait needs an input request and an effect");
|
|
63
|
+
if (signal.aborted) return run();
|
|
64
|
+
const requestId = id();
|
|
65
|
+
const content = capture();
|
|
66
|
+
content.truncated ||=
|
|
67
|
+
(request.questions?.length ?? 0) > 128 ||
|
|
68
|
+
(request.questions ?? [])
|
|
69
|
+
.slice(0, 128)
|
|
70
|
+
.some((question) => (question.options?.length ?? 0) > 128);
|
|
71
|
+
publish(
|
|
72
|
+
"wait.start",
|
|
73
|
+
{
|
|
74
|
+
requestId,
|
|
75
|
+
kind: request.kind,
|
|
76
|
+
callId,
|
|
77
|
+
externalId: request.externalId ?? null,
|
|
78
|
+
server: request.server ?? null,
|
|
79
|
+
schemaJson: content.text(request.schemaJson),
|
|
80
|
+
prompt: content.text(request.prompt),
|
|
81
|
+
questionCount: request.questions?.length ?? 0,
|
|
82
|
+
...(hooks.captureContent === true
|
|
83
|
+
? {
|
|
84
|
+
questions: (request.questions ?? []).slice(0, 128).map((question) => ({
|
|
85
|
+
question: content.text(question.question),
|
|
86
|
+
options: (question.options ?? []).slice(0, 128).map((option) => ({
|
|
87
|
+
label: content.text(option.label),
|
|
88
|
+
description: content.text(option.description),
|
|
89
|
+
})),
|
|
90
|
+
})),
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
},
|
|
94
|
+
content,
|
|
95
|
+
);
|
|
96
|
+
const finish = terminal((outcome, result, error) => {
|
|
97
|
+
const content = capture();
|
|
98
|
+
content.truncated ||= (result?.answers?.length ?? 0) > 128;
|
|
99
|
+
publish(
|
|
100
|
+
"wait.end",
|
|
101
|
+
{
|
|
102
|
+
requestId,
|
|
103
|
+
outcome,
|
|
104
|
+
error: error?.name ?? null,
|
|
105
|
+
...(hooks.captureContent === true
|
|
106
|
+
? {
|
|
107
|
+
answers: (result?.answers ?? [])
|
|
108
|
+
.slice(0, 128)
|
|
109
|
+
.map((answer) => content.text(answer)),
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
},
|
|
113
|
+
content,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const result = await run();
|
|
118
|
+
let outcome = "accepted";
|
|
119
|
+
try {
|
|
120
|
+
outcome = result?.outcome ?? outcome;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
report(error);
|
|
123
|
+
}
|
|
124
|
+
finish(outcome, result);
|
|
125
|
+
return result;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
finish(
|
|
128
|
+
signal.aborted || error?.name === "AbortError"
|
|
129
|
+
? "cancelled"
|
|
130
|
+
: error?.name === "TimeoutError"
|
|
131
|
+
? "timed_out"
|
|
132
|
+
: "failed",
|
|
133
|
+
undefined,
|
|
134
|
+
error,
|
|
135
|
+
);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async mcp(identity, run) {
|
|
140
|
+
if (signal.aborted) return run({ correlationId: identity.correlationId });
|
|
141
|
+
const operationId = id();
|
|
142
|
+
publish("mcp.start", {
|
|
143
|
+
operationId,
|
|
144
|
+
callId,
|
|
145
|
+
server: identity.server ?? null,
|
|
146
|
+
tool: identity.tool,
|
|
147
|
+
transport: null,
|
|
148
|
+
});
|
|
149
|
+
if (identity.correlationId !== undefined)
|
|
150
|
+
publish("mcp.request", {
|
|
151
|
+
operationId,
|
|
152
|
+
requestId: identity.correlationId,
|
|
153
|
+
requestIdKind: "correlation",
|
|
154
|
+
});
|
|
155
|
+
const finish = terminal((status, error) =>
|
|
156
|
+
publish("mcp.end", { operationId, status, error: error?.name ?? null }),
|
|
157
|
+
);
|
|
158
|
+
try {
|
|
159
|
+
const result = await run({ correlationId: identity.correlationId });
|
|
160
|
+
let status = "unknown";
|
|
161
|
+
try {
|
|
162
|
+
status = result?.isError ? "failure" : "success";
|
|
163
|
+
} catch (error) {
|
|
164
|
+
report(error);
|
|
165
|
+
}
|
|
166
|
+
finish(status);
|
|
167
|
+
return result;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
finish(signal.aborted || error?.name === "AbortError" ? "cancelled" : "failure", error);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
childHooks() {
|
|
174
|
+
return {
|
|
175
|
+
...hooks,
|
|
176
|
+
parent: {
|
|
177
|
+
rootSessionId: scope.rootSessionId,
|
|
178
|
+
sessionId: scope.sessionId,
|
|
179
|
+
turnId: scope.turnId,
|
|
180
|
+
callId,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// `libfx` in browsers and other non-Node hosts, and `libfx/browser` (browser.js).
|
|
2
|
+
import type {
|
|
3
|
+
FxAgent,
|
|
4
|
+
FxAgentOptions,
|
|
5
|
+
FxBrowserBackendOptions,
|
|
6
|
+
FxEngine,
|
|
7
|
+
FxEngineOptions,
|
|
8
|
+
FxTerminal,
|
|
9
|
+
FxTerminalOptions,
|
|
10
|
+
} from "./libfx.cjs";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createMemoryPersistence,
|
|
14
|
+
encodeXtermKeyEvent,
|
|
15
|
+
FxFencedError,
|
|
16
|
+
FxJournalVersionError,
|
|
17
|
+
fxSdkApiVersion,
|
|
18
|
+
listModels,
|
|
19
|
+
memory,
|
|
20
|
+
supportsJspi,
|
|
21
|
+
xtermAdapter,
|
|
22
|
+
} from "./libfx.cjs";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
Durability,
|
|
26
|
+
FxAccepted,
|
|
27
|
+
FxAgent,
|
|
28
|
+
FxAgentEvent,
|
|
29
|
+
FxAgentHostTool,
|
|
30
|
+
FxAgentKeyedTool,
|
|
31
|
+
FxAgentOptions,
|
|
32
|
+
FxAgentTool,
|
|
33
|
+
FxAgentTools,
|
|
34
|
+
FxApiKeyCredentials,
|
|
35
|
+
FxBrowserBackendOptions,
|
|
36
|
+
FxBytes,
|
|
37
|
+
FxCheckpointMeta,
|
|
38
|
+
FxCodexProvider,
|
|
39
|
+
FxCustomProvider,
|
|
40
|
+
FxCustomProviderDefinition,
|
|
41
|
+
FxCustomProviderModel,
|
|
42
|
+
FxEffort,
|
|
43
|
+
FxEngine,
|
|
44
|
+
FxEngineEvent,
|
|
45
|
+
FxEngineOptions,
|
|
46
|
+
FxEnginePromptOptions,
|
|
47
|
+
FxEngineStorage,
|
|
48
|
+
FxEngineTurn,
|
|
49
|
+
FxEngineTurnResult,
|
|
50
|
+
FxErrorSummary,
|
|
51
|
+
FxFetch,
|
|
52
|
+
FxFollowUp,
|
|
53
|
+
FxGatewayProvider,
|
|
54
|
+
FxGrokProvider,
|
|
55
|
+
FxHostTool,
|
|
56
|
+
FxHooks,
|
|
57
|
+
FxHookEvent,
|
|
58
|
+
FxHookMessage,
|
|
59
|
+
FxHookToolCall,
|
|
60
|
+
FxIdleResult,
|
|
61
|
+
FxImageBlock,
|
|
62
|
+
FxJsonSchema,
|
|
63
|
+
FxKeyedTool,
|
|
64
|
+
FxKeyEventLike,
|
|
65
|
+
FxListModelsOptions,
|
|
66
|
+
FxModelCatalogEntry,
|
|
67
|
+
FxModelChoice,
|
|
68
|
+
FxModelOptions,
|
|
69
|
+
FxOnAmbiguous,
|
|
70
|
+
FxPromptBlock,
|
|
71
|
+
FxPromptInput,
|
|
72
|
+
FxPromptOptions,
|
|
73
|
+
FxProviderCredentials,
|
|
74
|
+
FxProviderTool,
|
|
75
|
+
FxResizedImage,
|
|
76
|
+
FxResourceBlock,
|
|
77
|
+
FxResumeOptions,
|
|
78
|
+
FxResumeTurn,
|
|
79
|
+
FxSession,
|
|
80
|
+
FxSessionEvent,
|
|
81
|
+
FxSessionImageBlock,
|
|
82
|
+
FxSessionOptions,
|
|
83
|
+
FxSessionPromptInput,
|
|
84
|
+
FxSessionTurnEvent,
|
|
85
|
+
FxSharedOptions,
|
|
86
|
+
FxSteer,
|
|
87
|
+
FxSteerOptions,
|
|
88
|
+
FxSteered,
|
|
89
|
+
FxSteeringInput,
|
|
90
|
+
FxStopReason,
|
|
91
|
+
FxStreamLine,
|
|
92
|
+
FxTerminal,
|
|
93
|
+
FxTerminalAdapter,
|
|
94
|
+
FxTerminalEvent,
|
|
95
|
+
FxTerminalOptions,
|
|
96
|
+
FxTextBlock,
|
|
97
|
+
FxTool,
|
|
98
|
+
FxToolContext,
|
|
99
|
+
FxToolObservation,
|
|
100
|
+
FxWaitRequest,
|
|
101
|
+
FxWaitResponse,
|
|
102
|
+
FxToolImage,
|
|
103
|
+
FxToolResult,
|
|
104
|
+
FxTools,
|
|
105
|
+
FxTurn,
|
|
106
|
+
FxTurnEvent,
|
|
107
|
+
FxTurnResult,
|
|
108
|
+
FxTurnView,
|
|
109
|
+
FxUsage,
|
|
110
|
+
FxWasmModule,
|
|
111
|
+
FxWasmSource,
|
|
112
|
+
FxWorkspace,
|
|
113
|
+
FxWorkspaceExecResult,
|
|
114
|
+
FxXtermLike,
|
|
115
|
+
JournalRecord,
|
|
116
|
+
JsonObject,
|
|
117
|
+
JsonValue,
|
|
118
|
+
MemoryOptions,
|
|
119
|
+
MemoryPersistence,
|
|
120
|
+
Persistence,
|
|
121
|
+
PersistenceLoadResult,
|
|
122
|
+
} from "./libfx.cjs";
|
|
123
|
+
|
|
124
|
+
/** Options for `createFxAgent()` in browsers. */
|
|
125
|
+
export type FxBrowserAgentOptions = FxAgentOptions & FxBrowserBackendOptions;
|
|
126
|
+
|
|
127
|
+
/** Options for `createFxEngine()` in browsers. */
|
|
128
|
+
export type FxBrowserEngineOptions = FxEngineOptions & FxBrowserBackendOptions;
|
|
129
|
+
|
|
130
|
+
/** Options for `createFxTerminal()` in browsers. */
|
|
131
|
+
export type FxBrowserTerminalOptions = FxTerminalOptions & FxBrowserBackendOptions;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An agent whose sessions live in this page unless `durability` says
|
|
135
|
+
* otherwise. It returns at once and does no I/O until a session runs a turn.
|
|
136
|
+
*/
|
|
137
|
+
export declare function createFxAgent(options?: FxBrowserAgentOptions): FxAgent;
|
|
138
|
+
|
|
139
|
+
/** One fx session on WebAssembly, with no durability of its own. `apiKey` is required. */
|
|
140
|
+
export declare function createFxEngine(options: FxBrowserEngineOptions): Promise<FxEngine>;
|
|
141
|
+
|
|
142
|
+
/** The interactive terminal harness. Connect it to xterm.js with `xtermAdapter()`. */
|
|
143
|
+
export declare function createFxTerminal(options: FxBrowserTerminalOptions): Promise<FxTerminal>;
|
|
144
|
+
|
|
145
|
+
/** The version of the libfx agent API. */
|
|
146
|
+
export declare const libfxApiVersion: 3;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// `libfx/chatgpt` (chatgpt-sign-in.js): sign in with ChatGPT for a Codex agent.
|
|
2
|
+
import type { FxProviderCredentials, FxFetch } from "./libfx.cjs";
|
|
3
|
+
|
|
4
|
+
/** Options every network step accepts. */
|
|
5
|
+
export interface ChatgptRequestOptions {
|
|
6
|
+
/** Replaces the global `fetch` for OpenAI's sign-in endpoints. */
|
|
7
|
+
fetch?: FxFetch;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A sign-in in progress. Keep it on the server: `verifier` is secret. */
|
|
12
|
+
export interface ChatgptSignIn {
|
|
13
|
+
/** Where the user signs in. Afterwards OpenAI opens an address at localhost:1455 that does not load; the user pastes it back. */
|
|
14
|
+
readonly url: string;
|
|
15
|
+
readonly state: string;
|
|
16
|
+
readonly verifier: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A device code sign-in in progress. Plain data, so a later request can poll it. */
|
|
20
|
+
export interface ChatgptDeviceSignIn {
|
|
21
|
+
/** Where the user enters `userCode`. */
|
|
22
|
+
readonly verificationUrl: string;
|
|
23
|
+
readonly userCode: string;
|
|
24
|
+
readonly deviceAuthId: string;
|
|
25
|
+
/** How often to poll, in milliseconds. */
|
|
26
|
+
readonly intervalMs: number;
|
|
27
|
+
/** Milliseconds since the epoch. */
|
|
28
|
+
readonly expiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The `code` of an error from this module. */
|
|
32
|
+
export type ChatgptSignInErrorCode =
|
|
33
|
+
| "LIBFX_CHATGPT_SIGN_IN_INVALID_ADDRESS"
|
|
34
|
+
| "LIBFX_CHATGPT_SIGN_IN_STATE_MISMATCH"
|
|
35
|
+
| "LIBFX_CHATGPT_SIGN_IN_DENIED"
|
|
36
|
+
| "LIBFX_CHATGPT_SIGN_IN_EXPIRED"
|
|
37
|
+
| "LIBFX_CHATGPT_SIGN_IN_FAILED"
|
|
38
|
+
| "LIBFX_CHATGPT_DEVICE_CODE_UNAVAILABLE"
|
|
39
|
+
| "LIBFX_CHATGPT_DEVICE_CODE_EXPIRED"
|
|
40
|
+
| "LIBFX_CHATGPT_REFRESH_FAILED";
|
|
41
|
+
|
|
42
|
+
/** Starts a sign-in where the user pastes back the address OpenAI opens. */
|
|
43
|
+
export declare function startSignIn(): Promise<ChatgptSignIn>;
|
|
44
|
+
|
|
45
|
+
/** Finishes a sign-in from the pasted address, its query, or either wrapped in quotes or backticks. */
|
|
46
|
+
export declare function finishSignIn(signIn: ChatgptSignIn, address: string, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
|
|
47
|
+
|
|
48
|
+
/** Starts a device code sign-in. The account or workspace must allow device code sign-in. */
|
|
49
|
+
export declare function startDeviceSignIn(options?: ChatgptRequestOptions): Promise<ChatgptDeviceSignIn>;
|
|
50
|
+
|
|
51
|
+
/** Checks once: credentials after the user entered the code, otherwise null. */
|
|
52
|
+
export declare function pollDeviceSignIn(device: ChatgptDeviceSignIn, options?: ChatgptRequestOptions): Promise<FxProviderCredentials | null>;
|
|
53
|
+
|
|
54
|
+
/** Waits until the user enters the code, polling at OpenAI's interval. */
|
|
55
|
+
export declare function completeDeviceSignIn(device: ChatgptDeviceSignIn, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
|
|
56
|
+
|
|
57
|
+
/** Refreshes credentials. Codex agents refresh on their own; this is for hosts. */
|
|
58
|
+
export declare function refreshCredentials(credentials: FxProviderCredentials, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `libfx/durable-local` (durable/local.mjs): sessions in files on this
|
|
2
|
+
// machine, through the Workflow local World libfx bundles.
|
|
3
|
+
import type { Durability } from "./libfx.cjs";
|
|
4
|
+
|
|
5
|
+
export type { Durability } from "./libfx.cjs";
|
|
6
|
+
|
|
7
|
+
/** Options for `local()`. */
|
|
8
|
+
export interface LocalOptions {
|
|
9
|
+
/** Where the sessions live. */
|
|
10
|
+
dir?: string;
|
|
11
|
+
/** Each delivery runs as if its function stopped after this long, as on Vercel. No limit by default. */
|
|
12
|
+
maxDurationMs?: number;
|
|
13
|
+
/** How long before the deadline each delivery stops. Default 30000. */
|
|
14
|
+
reserveMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Keeps sessions in files on this machine. A session held by a process that died is free at once. */
|
|
18
|
+
export declare function local(options?: LocalOptions): Durability;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// `libfx/durable-vercel` (durable/vercel.mjs): sessions in Vercel's World,
|
|
2
|
+
// the store and queue behind Vercel Workflow, which libfx bundles.
|
|
3
|
+
import type { Durability } from "./libfx.cjs";
|
|
4
|
+
|
|
5
|
+
export type { Durability } from "./libfx.cjs";
|
|
6
|
+
|
|
7
|
+
/** Options for `vercel()`. */
|
|
8
|
+
export interface VercelOptions {
|
|
9
|
+
/** How long before the function's deadline each delivery stops. Default 30000. */
|
|
10
|
+
reserveMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Renew a lease of this length in the session's log while a turn runs, every third of it, so a function that stopped
|
|
13
|
+
* frees its session this long after its last renewal. Without it, nothing is renewed: a function holds its session
|
|
14
|
+
* for as long as Vercel Queues keeps the turn's message for it, and a function that died frees its session when the
|
|
15
|
+
* queue delivers that message again, about a second after its process exits, or once the queue's hold on it runs
|
|
16
|
+
* out. At least 1000.
|
|
17
|
+
*/
|
|
18
|
+
leaseMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Keeps sessions in Vercel's World. A function holds its session while Vercel Queues keeps its message for it, and the next delivery of that message continues a turn whose function died. */
|
|
22
|
+
export declare function vercel(options?: VercelOptions): Durability;
|