@rivet-dev/agentos-browser 0.2.4 → 0.2.5-rc.1
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/dist/agent-drive-loop.d.ts +29 -0
- package/dist/agent-drive-loop.js +43 -0
- package/dist/chrome-llm-adapter.d.ts +55 -0
- package/dist/chrome-llm-adapter.js +135 -0
- package/dist/converged-execution-host-bridge.d.ts +27 -0
- package/dist/converged-execution-host-bridge.js +180 -0
- package/dist/converged-sidecar.d.ts +39 -0
- package/dist/converged-sidecar.js +60 -0
- package/dist/index.d.ts +12 -9
- package/dist/index.js +19 -5
- package/dist/openai-proxy.d.ts +23 -0
- package/dist/openai-proxy.js +115 -0
- package/package.json +18 -40
- package/dist/driver.d.ts +0 -114
- package/dist/driver.js +0 -475
- package/dist/os-filesystem.d.ts +0 -47
- package/dist/os-filesystem.js +0 -402
- package/dist/permission-validation.d.ts +0 -15
- package/dist/permission-validation.js +0 -79
- package/dist/runtime-driver.d.ts +0 -34
- package/dist/runtime-driver.js +0 -459
- package/dist/runtime.d.ts +0 -222
- package/dist/runtime.js +0 -415
- package/dist/sync-bridge.d.ts +0 -46
- package/dist/sync-bridge.js +0 -49
- package/dist/worker-adapter.d.ts +0 -21
- package/dist/worker-adapter.js +0 -41
- package/dist/worker-protocol.d.ts +0 -93
- package/dist/worker-protocol.js +0 -1
- package/dist/worker.d.ts +0 -1
- package/dist/worker.js +0 -1408
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type AgentOutputKind = "stdout" | "stderr" | "exit";
|
|
2
|
+
export interface AgentOutput {
|
|
3
|
+
kind: AgentOutputKind;
|
|
4
|
+
payload: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export interface DriveResult {
|
|
7
|
+
/** True while the interaction is still in flight (AcpPendingResponse). */
|
|
8
|
+
pending: boolean;
|
|
9
|
+
/** The decoded response frame bytes (the real ACP result when !pending). */
|
|
10
|
+
response: Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
export interface AgentDriveDeps {
|
|
13
|
+
/** Read the next output for this execution from the reactor, servicing the
|
|
14
|
+
* agent's syscalls while waiting; null on timeout. */
|
|
15
|
+
pollAgentOutput(processId: string, deadlineMs: number): AgentOutput | null;
|
|
16
|
+
/** Feed a chunk of agent stdout into the resumable handshake (a non-nested
|
|
17
|
+
* pushFrame) and return whether it is still pending + the response frame. */
|
|
18
|
+
deliverAgentOutput(processId: string, chunk: Uint8Array): DriveResult;
|
|
19
|
+
now(): number;
|
|
20
|
+
}
|
|
21
|
+
export declare class AgentInteractionError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Drive a resumable ACP interaction to completion. Returns the real ACP result
|
|
26
|
+
* frame bytes (the deferred response the kernel worker relays to the client).
|
|
27
|
+
* Throws if the agent exits or times out before producing a result.
|
|
28
|
+
*/
|
|
29
|
+
export declare function driveAgentInteraction(deps: AgentDriveDeps, processId: string, deadlineMs: number): Uint8Array;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The kernel-worker drive loop for a resumable ACP interaction (AGENTOS-WEB-ASYNC-
|
|
2
|
+
// AGENTS.md §3.2.1). After a create_session / session/prompt wire frame returns
|
|
3
|
+
// AcpPending{processId}, the kernel worker drives the agent to completion: it reads
|
|
4
|
+
// the agent's stdout from the reactor (which services the agent's own syscalls in
|
|
5
|
+
// the meantime — legal, because we are NOT inside a pushFrame here) and feeds each
|
|
6
|
+
// chunk back via deliver_agent_output (a fresh, non-nested pushFrame) until the
|
|
7
|
+
// handshake/turn completes and yields the real result.
|
|
8
|
+
//
|
|
9
|
+
// This is pure orchestration over injected dependencies, so it is unit-testable
|
|
10
|
+
// without the wasm kernel, the reactor, or real workers.
|
|
11
|
+
export class AgentInteractionError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AgentInteractionError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drive a resumable ACP interaction to completion. Returns the real ACP result
|
|
19
|
+
* frame bytes (the deferred response the kernel worker relays to the client).
|
|
20
|
+
* Throws if the agent exits or times out before producing a result.
|
|
21
|
+
*/
|
|
22
|
+
export function driveAgentInteraction(deps, processId, deadlineMs) {
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (deps.now() >= deadlineMs) {
|
|
25
|
+
throw new AgentInteractionError(`agent interaction timed out (${processId})`);
|
|
26
|
+
}
|
|
27
|
+
const out = deps.pollAgentOutput(processId, deadlineMs);
|
|
28
|
+
if (out === null) {
|
|
29
|
+
throw new AgentInteractionError(`agent produced no output before the deadline (${processId})`);
|
|
30
|
+
}
|
|
31
|
+
if (out.kind === "exit") {
|
|
32
|
+
throw new AgentInteractionError(`agent exited before completing the ACP interaction (${processId})`);
|
|
33
|
+
}
|
|
34
|
+
if (out.kind === "stderr") {
|
|
35
|
+
// stderr is diagnostic; it does not advance the JSON-RPC handshake.
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const result = deps.deliverAgentOutput(processId, out.payload);
|
|
39
|
+
if (!result.pending) {
|
|
40
|
+
return result.response;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The slice of Chrome's built-in `LanguageModel` we use (a created session). */
|
|
2
|
+
export interface LanguageModelSession {
|
|
3
|
+
prompt(input: string): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export declare const LANGUAGE_MODEL_OPTIONS: {
|
|
6
|
+
expectedInputs: {
|
|
7
|
+
type: string;
|
|
8
|
+
languages: string[];
|
|
9
|
+
}[];
|
|
10
|
+
expectedOutputs: {
|
|
11
|
+
type: string;
|
|
12
|
+
languages: string[];
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
export interface ChromeLanguageModelSessionOptions {
|
|
16
|
+
allowDownload?: boolean;
|
|
17
|
+
onDownloadProgress?(progress: number): void;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
export declare function getChromeLanguageModelAvailability(): Promise<string>;
|
|
21
|
+
/** Minimal chat request shape — accepts OpenAI (`messages`) and Anthropic
|
|
22
|
+
* (`messages` + top-level `system`) bodies; both are `{role, content}` arrays. */
|
|
23
|
+
interface ChatRequest {
|
|
24
|
+
model?: string;
|
|
25
|
+
system?: string;
|
|
26
|
+
messages?: {
|
|
27
|
+
role: string;
|
|
28
|
+
content: unknown;
|
|
29
|
+
}[];
|
|
30
|
+
prompt?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Flatten a chat request into a single prompt string for the on-device model. */
|
|
33
|
+
export declare function chatRequestToPrompt(request: ChatRequest): string;
|
|
34
|
+
/**
|
|
35
|
+
* Handle one chat-completion request against the on-device model. Returns an
|
|
36
|
+
* OpenAI-chat-completion-shaped JSON string (the shape pi's client decodes).
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleChatCompletion(requestBody: string, session: LanguageModelSession): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* MANUAL-TESTING STAND-IN ONLY — not a real model. Returns a fake
|
|
41
|
+
* `LanguageModelSession` whose `prompt()` yields a deterministic canned reply
|
|
42
|
+
* instead of calling Chrome's on-device model. This exists so the real terminal
|
|
43
|
+
* + pi TUI flow can be driven end-to-end on hosts where `window.LanguageModel`
|
|
44
|
+
* cannot be provisioned (e.g. a headless Linux box). It is OPT-IN and is NEVER
|
|
45
|
+
* used by the strict real-model gates (`verify:real-pi-model` /
|
|
46
|
+
* `verify:real-language-model`), which require a genuine `window.LanguageModel`
|
|
47
|
+
* answer and fail honestly when it is absent. Callers that use this must report
|
|
48
|
+
* `usedRealLanguageModel: false`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createMockLanguageModelSession(reply?: (prompt: string) => string): LanguageModelSession;
|
|
51
|
+
/** Create a `LanguageModelSession` from the real Chrome `LanguageModel` global, or
|
|
52
|
+
* return null if it is unavailable (CI / unsupported). Probed lazily so the bundle
|
|
53
|
+
* loads without it. */
|
|
54
|
+
export declare function createChromeLanguageModelSession(options?: ChromeLanguageModelSessionOptions): Promise<LanguageModelSession | null>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// The chrome-llm host-callback handler core (AGENTOS-WEB-ASYNC-AGENTS.md §6): the
|
|
2
|
+
// trusted main-thread adapter that turns a chat-completion request (the shape pi's
|
|
3
|
+
// HTTP client sends to its baseUrl) into a Chrome built-in `LanguageModel` call and
|
|
4
|
+
// formats the reply back. This is the browser twin of native's `llmock-server.mjs`,
|
|
5
|
+
// except backed by on-device inference instead of a mock/remote API.
|
|
6
|
+
//
|
|
7
|
+
// It is the only NEW trusted code in the inference path: the in-sandbox proxy guest
|
|
8
|
+
// reaches it via the existing kernel-brokered host-callback (so it is mediated by
|
|
9
|
+
// the policy, not an ambient capability), and pi reaches the proxy over the kernel
|
|
10
|
+
// loopback. pi needs ZERO changes beyond a baseUrl.
|
|
11
|
+
//
|
|
12
|
+
// Pure + injectable: the model is an interface, so a mock (returning a fixed
|
|
13
|
+
// sentinel) drives the deterministic CI gate and the real `LanguageModel` drives
|
|
14
|
+
// the best-effort Nano smoke. Non-streaming for now (single prompt → single reply).
|
|
15
|
+
export const LANGUAGE_MODEL_OPTIONS = {
|
|
16
|
+
expectedInputs: [{ type: "text", languages: ["en"] }],
|
|
17
|
+
expectedOutputs: [{ type: "text", languages: ["en"] }],
|
|
18
|
+
};
|
|
19
|
+
function getLanguageModelGlobal() {
|
|
20
|
+
return globalThis.LanguageModel;
|
|
21
|
+
}
|
|
22
|
+
export async function getChromeLanguageModelAvailability() {
|
|
23
|
+
const LanguageModel = getLanguageModelGlobal();
|
|
24
|
+
if (!LanguageModel)
|
|
25
|
+
return "missing-global";
|
|
26
|
+
try {
|
|
27
|
+
return await LanguageModel.availability(LANGUAGE_MODEL_OPTIONS);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return `availability-error:${error instanceof Error ? error.message : String(error)}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function contentToText(content) {
|
|
34
|
+
if (typeof content === "string")
|
|
35
|
+
return content;
|
|
36
|
+
// Anthropic/OpenAI content can be an array of parts ({type:"text", text}).
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
return content
|
|
39
|
+
.map((part) => part && typeof part === "object" && "text" in part
|
|
40
|
+
? String(part.text)
|
|
41
|
+
: typeof part === "string"
|
|
42
|
+
? part
|
|
43
|
+
: "")
|
|
44
|
+
.join("");
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
/** Flatten a chat request into a single prompt string for the on-device model. */
|
|
49
|
+
export function chatRequestToPrompt(request) {
|
|
50
|
+
if (typeof request.prompt === "string")
|
|
51
|
+
return request.prompt;
|
|
52
|
+
const lines = [];
|
|
53
|
+
if (request.system)
|
|
54
|
+
lines.push(`system: ${request.system}`);
|
|
55
|
+
for (const message of request.messages ?? []) {
|
|
56
|
+
lines.push(`${message.role}: ${contentToText(message.content)}`);
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Handle one chat-completion request against the on-device model. Returns an
|
|
62
|
+
* OpenAI-chat-completion-shaped JSON string (the shape pi's client decodes).
|
|
63
|
+
*/
|
|
64
|
+
export async function handleChatCompletion(requestBody, session) {
|
|
65
|
+
let request;
|
|
66
|
+
try {
|
|
67
|
+
request = JSON.parse(requestBody);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return JSON.stringify({ error: { type: "invalid_request", message: "invalid JSON body" } });
|
|
71
|
+
}
|
|
72
|
+
const text = await session.prompt(chatRequestToPrompt(request));
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
id: "chatcmpl-chrome-local",
|
|
75
|
+
object: "chat.completion",
|
|
76
|
+
model: request.model ?? "chrome-local",
|
|
77
|
+
choices: [
|
|
78
|
+
{
|
|
79
|
+
index: 0,
|
|
80
|
+
message: { role: "assistant", content: text },
|
|
81
|
+
finish_reason: "stop",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* MANUAL-TESTING STAND-IN ONLY — not a real model. Returns a fake
|
|
88
|
+
* `LanguageModelSession` whose `prompt()` yields a deterministic canned reply
|
|
89
|
+
* instead of calling Chrome's on-device model. This exists so the real terminal
|
|
90
|
+
* + pi TUI flow can be driven end-to-end on hosts where `window.LanguageModel`
|
|
91
|
+
* cannot be provisioned (e.g. a headless Linux box). It is OPT-IN and is NEVER
|
|
92
|
+
* used by the strict real-model gates (`verify:real-pi-model` /
|
|
93
|
+
* `verify:real-language-model`), which require a genuine `window.LanguageModel`
|
|
94
|
+
* answer and fail honestly when it is absent. Callers that use this must report
|
|
95
|
+
* `usedRealLanguageModel: false`.
|
|
96
|
+
*/
|
|
97
|
+
export function createMockLanguageModelSession(reply) {
|
|
98
|
+
return {
|
|
99
|
+
async prompt(input) {
|
|
100
|
+
if (reply)
|
|
101
|
+
return reply(input);
|
|
102
|
+
const lastUser = input
|
|
103
|
+
.split("\n")
|
|
104
|
+
.reverse()
|
|
105
|
+
.find((line) => line.trimStart().startsWith("user:"));
|
|
106
|
+
const echo = (lastUser ?? input).replace(/^\s*user:\s*/, "").trim();
|
|
107
|
+
return ("[MOCK on-device model — not real] No real window.LanguageModel is " +
|
|
108
|
+
"available on this host, so this is a placeholder reply for manual " +
|
|
109
|
+
`testing. You said: ${echo || "(nothing)"}`);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Create a `LanguageModelSession` from the real Chrome `LanguageModel` global, or
|
|
114
|
+
* return null if it is unavailable (CI / unsupported). Probed lazily so the bundle
|
|
115
|
+
* loads without it. */
|
|
116
|
+
export async function createChromeLanguageModelSession(options = {}) {
|
|
117
|
+
const LanguageModel = getLanguageModelGlobal();
|
|
118
|
+
if (!LanguageModel)
|
|
119
|
+
return null;
|
|
120
|
+
const availability = await getChromeLanguageModelAvailability();
|
|
121
|
+
if (availability !== "available" &&
|
|
122
|
+
!(options.allowDownload && (availability === "downloadable" || availability === "downloading"))) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return LanguageModel.create({
|
|
126
|
+
...LANGUAGE_MODEL_OPTIONS,
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
monitor(monitor) {
|
|
129
|
+
monitor.addEventListener("downloadprogress", (event) => {
|
|
130
|
+
const progress = Number(event.loaded ?? 0);
|
|
131
|
+
options.onDownloadProgress?.(progress);
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A synchronous ACP agent: each newline-delimited stdin JSON-RPC line maps to
|
|
2
|
+
* zero or more response lines, computed with no async I/O. */
|
|
3
|
+
export interface SyncAgent {
|
|
4
|
+
/** Handle one complete stdin line (no trailing newline); return response lines. */
|
|
5
|
+
handleLine(line: string): string[];
|
|
6
|
+
}
|
|
7
|
+
/** Creates a {@link SyncAgent} for one execution (the launched ACP adapter). */
|
|
8
|
+
export interface SyncAgentExecutor {
|
|
9
|
+
createAgent(request: {
|
|
10
|
+
executionId: string;
|
|
11
|
+
vmId: string;
|
|
12
|
+
argv: string[];
|
|
13
|
+
env: Record<string, string>;
|
|
14
|
+
cwd: string;
|
|
15
|
+
}): SyncAgent;
|
|
16
|
+
}
|
|
17
|
+
export interface ConvergedExecutionHostBridgeOptions {
|
|
18
|
+
/** When set, `startExecution` runs a synchronous in-process agent (AGENT mode). */
|
|
19
|
+
agentExecutor?: SyncAgentExecutor;
|
|
20
|
+
}
|
|
21
|
+
export interface ConvergedExecutionHostBridge {
|
|
22
|
+
/** Set the execution id `startExecution` echoes next (DRIVER mode). */
|
|
23
|
+
setNextExecutionId(executionId: string): void;
|
|
24
|
+
/** The host-bridge object passed to `new AgentOsBrowserSidecarWasm(bridge)`. */
|
|
25
|
+
readonly bridge: Record<string, (requestJson: string) => unknown>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createConvergedExecutionHostBridge(options?: ConvergedExecutionHostBridgeOptions): ConvergedExecutionHostBridge;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Execution host bridge for the converged Agent OS wasm sidecar.
|
|
2
|
+
//
|
|
3
|
+
// Two roles, selected by configuration:
|
|
4
|
+
//
|
|
5
|
+
// 1. DRIVER mode (default, no `agentExecutor`): mirrors secure-exec's no-op
|
|
6
|
+
// execution host bridge. In the converged browser runtime the guest runs in the
|
|
7
|
+
// browser worker (driven by @secure-exec/browser's runtime driver), not in the
|
|
8
|
+
// wasm sidecar; the sidecar only needs a kernel process (pid) for socket
|
|
9
|
+
// ownership, created by an `execute` wire request. `startExecution` echoes the
|
|
10
|
+
// driver-provided execution id (set via `setNextExecutionId`); the stdio
|
|
11
|
+
// callbacks are no-ops (the driver owns the guest's stdio).
|
|
12
|
+
//
|
|
13
|
+
// 2. AGENT mode (`agentExecutor` provided): runs a SYNCHRONOUS in-process agent for
|
|
14
|
+
// ACP `create_session`. Because the converged sidecar + the host-free `AcpCore`
|
|
15
|
+
// run synchronously on the main thread (a single `pushFrame` drives the whole
|
|
16
|
+
// initialize+session/new handshake), and the main thread may NOT block-wait on a
|
|
17
|
+
// Worker (`Atomics.wait` is forbidden there; postMessage can't arrive mid-frame),
|
|
18
|
+
// an *async* agent worker cannot be driven from here — see
|
|
19
|
+
// AGENTOS-WEB-CONVERGENCE.md Step 7. A synchronous agent (each stdin line maps to
|
|
20
|
+
// response lines with no async I/O, e.g. an ACP echo/test adapter) CAN: it runs
|
|
21
|
+
// in the same call stack, so `writeExecutionStdin` synchronously produces the
|
|
22
|
+
// output that the immediately-following `pollExecutionEvent` returns, and the
|
|
23
|
+
// synchronous `AcpCore` completes within the one `pushFrame`.
|
|
24
|
+
//
|
|
25
|
+
// The wasm `AgentOsBrowserSidecarWasm` host bridge invokes each method with a JSON
|
|
26
|
+
// request string and JSON-decodes the return value.
|
|
27
|
+
function decodeBase64ToText(base64) {
|
|
28
|
+
const binary = atob(base64);
|
|
29
|
+
const bytes = new Uint8Array(binary.length);
|
|
30
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
31
|
+
bytes[i] = binary.charCodeAt(i);
|
|
32
|
+
return new TextDecoder().decode(bytes);
|
|
33
|
+
}
|
|
34
|
+
function encodeTextToBase64(text) {
|
|
35
|
+
const bytes = new TextEncoder().encode(text);
|
|
36
|
+
let binary = "";
|
|
37
|
+
for (const byte of bytes)
|
|
38
|
+
binary += String.fromCharCode(byte);
|
|
39
|
+
return btoa(binary);
|
|
40
|
+
}
|
|
41
|
+
export function createConvergedExecutionHostBridge(options = {}) {
|
|
42
|
+
let nextExecutionId = "converged-exec";
|
|
43
|
+
let contextCounter = 0;
|
|
44
|
+
let workerCounter = 0;
|
|
45
|
+
let agentCounter = 0;
|
|
46
|
+
const executor = options.agentExecutor;
|
|
47
|
+
const sessions = new Map();
|
|
48
|
+
const bridge = {
|
|
49
|
+
createJavascriptContext() {
|
|
50
|
+
contextCounter += 1;
|
|
51
|
+
return { contextId: `converged-ctx-${contextCounter}` };
|
|
52
|
+
},
|
|
53
|
+
createWasmContext() {
|
|
54
|
+
contextCounter += 1;
|
|
55
|
+
return { contextId: `converged-wasm-ctx-${contextCounter}` };
|
|
56
|
+
},
|
|
57
|
+
startExecution(requestJson) {
|
|
58
|
+
if (!executor) {
|
|
59
|
+
// DRIVER mode: echo the driver-provided id.
|
|
60
|
+
return { executionId: nextExecutionId };
|
|
61
|
+
}
|
|
62
|
+
// AGENT mode: mint a fresh id and instantiate the synchronous agent.
|
|
63
|
+
const request = parse(requestJson);
|
|
64
|
+
agentCounter += 1;
|
|
65
|
+
const executionId = `agent-exec-${agentCounter}`;
|
|
66
|
+
const vmId = typeof request.vmId === "string" ? request.vmId : "";
|
|
67
|
+
const agent = executor.createAgent({
|
|
68
|
+
executionId,
|
|
69
|
+
vmId,
|
|
70
|
+
argv: Array.isArray(request.argv) ? request.argv : [],
|
|
71
|
+
env: request.env && typeof request.env === "object"
|
|
72
|
+
? request.env
|
|
73
|
+
: {},
|
|
74
|
+
cwd: typeof request.cwd === "string" ? request.cwd : "",
|
|
75
|
+
});
|
|
76
|
+
sessions.set(executionId, {
|
|
77
|
+
vmId,
|
|
78
|
+
agent,
|
|
79
|
+
buffer: "",
|
|
80
|
+
events: [],
|
|
81
|
+
exited: false,
|
|
82
|
+
});
|
|
83
|
+
return { executionId };
|
|
84
|
+
},
|
|
85
|
+
createWorker(requestJson) {
|
|
86
|
+
workerCounter += 1;
|
|
87
|
+
const request = parse(requestJson);
|
|
88
|
+
return {
|
|
89
|
+
workerId: `converged-worker-${workerCounter}`,
|
|
90
|
+
runtime: typeof request.runtime === "string" ? request.runtime : undefined,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
writeExecutionStdin(requestJson) {
|
|
94
|
+
const request = parse(requestJson);
|
|
95
|
+
const executionId = typeof request.executionId === "string" ? request.executionId : "";
|
|
96
|
+
const session = sessions.get(executionId);
|
|
97
|
+
if (!session)
|
|
98
|
+
return {};
|
|
99
|
+
const chunkBase64 = typeof request.chunkBase64 === "string" ? request.chunkBase64 : "";
|
|
100
|
+
session.buffer += decodeBase64ToText(chunkBase64);
|
|
101
|
+
let newlineIndex = session.buffer.indexOf("\n");
|
|
102
|
+
while (newlineIndex >= 0) {
|
|
103
|
+
const line = session.buffer.slice(0, newlineIndex);
|
|
104
|
+
session.buffer = session.buffer.slice(newlineIndex + 1);
|
|
105
|
+
if (line.trim()) {
|
|
106
|
+
for (const output of session.agent.handleLine(line)) {
|
|
107
|
+
session.events.push({
|
|
108
|
+
type: "stdout",
|
|
109
|
+
vmId: session.vmId,
|
|
110
|
+
executionId,
|
|
111
|
+
chunkBase64: encodeTextToBase64(`${output}\n`),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
newlineIndex = session.buffer.indexOf("\n");
|
|
116
|
+
}
|
|
117
|
+
return {};
|
|
118
|
+
},
|
|
119
|
+
closeExecutionStdin() {
|
|
120
|
+
return {};
|
|
121
|
+
},
|
|
122
|
+
killExecution(requestJson) {
|
|
123
|
+
const request = parse(requestJson);
|
|
124
|
+
const executionId = typeof request.executionId === "string" ? request.executionId : "";
|
|
125
|
+
const session = sessions.get(executionId);
|
|
126
|
+
if (session && !session.exited) {
|
|
127
|
+
session.exited = true;
|
|
128
|
+
session.events.push({
|
|
129
|
+
type: "exited",
|
|
130
|
+
vmId: session.vmId,
|
|
131
|
+
executionId,
|
|
132
|
+
exitCode: 0,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return {};
|
|
136
|
+
},
|
|
137
|
+
pollExecutionEvent(requestJson) {
|
|
138
|
+
const request = parse(requestJson);
|
|
139
|
+
const vmId = typeof request.vmId === "string" ? request.vmId : "";
|
|
140
|
+
for (const session of sessions.values()) {
|
|
141
|
+
if (session.vmId === vmId && session.events.length > 0) {
|
|
142
|
+
return session.events.shift();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
terminateWorker() {
|
|
148
|
+
return {};
|
|
149
|
+
},
|
|
150
|
+
emitStructuredEvent() {
|
|
151
|
+
return {};
|
|
152
|
+
},
|
|
153
|
+
emitDiagnostic() {
|
|
154
|
+
return {};
|
|
155
|
+
},
|
|
156
|
+
emitLog() {
|
|
157
|
+
return {};
|
|
158
|
+
},
|
|
159
|
+
emitLifecycle() {
|
|
160
|
+
return {};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
setNextExecutionId(executionId) {
|
|
165
|
+
nextExecutionId = executionId;
|
|
166
|
+
},
|
|
167
|
+
bridge,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function parse(requestJson) {
|
|
171
|
+
try {
|
|
172
|
+
const value = JSON.parse(requestJson);
|
|
173
|
+
return typeof value === "object" && value !== null
|
|
174
|
+
? value
|
|
175
|
+
: {};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CreateVmConfig } from "@secure-exec/core/vm-config";
|
|
2
|
+
import type { ProtocolFramePayloadCodec } from "@secure-exec/core/protocol-frames";
|
|
3
|
+
import type { ConvergedSidecarFactoryOptions } from "@secure-exec/browser";
|
|
4
|
+
import { type SyncAgentExecutor } from "./converged-execution-host-bridge.js";
|
|
5
|
+
export interface AgentOsConvergedSidecarOptions {
|
|
6
|
+
/** Wire codec; defaults to the same-version BARE codec. */
|
|
7
|
+
codec?: ProtocolFramePayloadCodec;
|
|
8
|
+
/** Invoked when the kernel denies a guest fs read with EACCES. */
|
|
9
|
+
onFsReadDenied?: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Override the wasm glue-module URL (advanced; defaults to the bundled
|
|
12
|
+
* dist/sidecar-wasm-web output resolved relative to this module).
|
|
13
|
+
*/
|
|
14
|
+
moduleUrl?: URL | string;
|
|
15
|
+
/** Override the wasm binary URL (advanced; see `moduleUrl`). */
|
|
16
|
+
binaryUrl?: URL | string;
|
|
17
|
+
/**
|
|
18
|
+
* Run a SYNCHRONOUS in-process ACP agent for `create_session` (e.g. an ACP
|
|
19
|
+
* echo/test adapter). Required to drive an agent process in-browser, because
|
|
20
|
+
* the synchronous main-thread `AcpCore` cannot block-wait on an async agent
|
|
21
|
+
* worker (see AGENTOS-WEB-CONVERGENCE.md Step 7). Omit for guest-only use.
|
|
22
|
+
*/
|
|
23
|
+
agentExecutor?: SyncAgentExecutor;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the {@link ConvergedSidecarFactoryOptions} for the Agent OS web-target
|
|
27
|
+
* wasm kernel (with the ACP extension). Pass the result to
|
|
28
|
+
* `createBrowserRuntimeDriverFactory`'s `convergedSidecar` option:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { createBrowserRuntimeDriverFactory } from "@secure-exec/browser";
|
|
32
|
+
* import { createAgentOsConvergedSidecar } from "@rivet-dev/agentos-browser";
|
|
33
|
+
*
|
|
34
|
+
* const factory = createBrowserRuntimeDriverFactory({
|
|
35
|
+
* convergedSidecar: createAgentOsConvergedSidecar(config),
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function createAgentOsConvergedSidecar(config: CreateVmConfig, options?: AgentOsConvergedSidecarOptions): ConvergedSidecarFactoryOptions;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Agent OS converged sidecar loader.
|
|
2
|
+
//
|
|
3
|
+
// The converged browser runtime lives in `@secure-exec/browser`: the worker, the
|
|
4
|
+
// SharedArrayBuffer sync-bridge, and the fs/net/dns/module servicers are all
|
|
5
|
+
// reused verbatim. Agent OS plugs in its OWN wasm sidecar — the one that registers
|
|
6
|
+
// `BrowserAcpExtension` — via `createBrowserRuntimeDriverFactory({ convergedSidecar })`.
|
|
7
|
+
// This is the Agent OS analogue of secure-exec's `createDefaultConvergedSidecar`:
|
|
8
|
+
// same `ConvergedSidecarFactoryOptions` contract, different (ACP-bearing) wasm.
|
|
9
|
+
//
|
|
10
|
+
// The kernel (wasm) remains the sole enforcement point; no guest-side permission
|
|
11
|
+
// eval, one transport. The agentos wasm-bindgen web output fetches its own
|
|
12
|
+
// `_bg.wasm`; both URLs resolve relative to this module so a consumer's bundler
|
|
13
|
+
// (or native ESM loader) emits/serves them.
|
|
14
|
+
import { createConvergedExecutionHostBridge, } from "./converged-execution-host-bridge.js";
|
|
15
|
+
const WASM_MODULE_URL = new URL("./sidecar-wasm-web/agentos_sidecar_browser.js", import.meta.url);
|
|
16
|
+
const WASM_BINARY_URL = new URL("./sidecar-wasm-web/agentos_sidecar_browser_bg.wasm", import.meta.url);
|
|
17
|
+
/**
|
|
18
|
+
* Build the {@link ConvergedSidecarFactoryOptions} for the Agent OS web-target
|
|
19
|
+
* wasm kernel (with the ACP extension). Pass the result to
|
|
20
|
+
* `createBrowserRuntimeDriverFactory`'s `convergedSidecar` option:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createBrowserRuntimeDriverFactory } from "@secure-exec/browser";
|
|
24
|
+
* import { createAgentOsConvergedSidecar } from "@rivet-dev/agentos-browser";
|
|
25
|
+
*
|
|
26
|
+
* const factory = createBrowserRuntimeDriverFactory({
|
|
27
|
+
* convergedSidecar: createAgentOsConvergedSidecar(config),
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createAgentOsConvergedSidecar(config, options = {}) {
|
|
32
|
+
const moduleUrl = options.moduleUrl ?? WASM_MODULE_URL;
|
|
33
|
+
const binaryUrl = options.binaryUrl ?? WASM_BINARY_URL;
|
|
34
|
+
return {
|
|
35
|
+
config,
|
|
36
|
+
codec: options.codec ?? "bare",
|
|
37
|
+
onFsReadDenied: options.onFsReadDenied,
|
|
38
|
+
async loadSidecar() {
|
|
39
|
+
const host = createConvergedExecutionHostBridge({
|
|
40
|
+
agentExecutor: options.agentExecutor,
|
|
41
|
+
});
|
|
42
|
+
const wasmModule = (await import(
|
|
43
|
+
/* @vite-ignore */ String(moduleUrl)));
|
|
44
|
+
await wasmModule.default(String(binaryUrl));
|
|
45
|
+
const sidecar = new wasmModule.AgentOsBrowserSidecarWasm(host.bridge);
|
|
46
|
+
return {
|
|
47
|
+
pushFrame: (frame) => {
|
|
48
|
+
const response = sidecar.pushFrame(frame);
|
|
49
|
+
if (!(response instanceof Uint8Array)) {
|
|
50
|
+
throw new Error("agentos wasm sidecar returned no response frame");
|
|
51
|
+
}
|
|
52
|
+
return response;
|
|
53
|
+
},
|
|
54
|
+
setNextExecutionId: (executionId) => {
|
|
55
|
+
host.setNextExecutionId(executionId);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
export type { BrowserDriverOptions, BrowserRuntimeSystemOptions, } from "
|
|
2
|
-
export { createBrowserDriver, createBrowserNetworkAdapter, createOpfsFileSystem,
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
-
export {
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
1
|
+
export type { BrowserDriverOptions, BrowserRuntimeSystemOptions, } from "@secure-exec/browser";
|
|
2
|
+
export { createBrowserDriver, createBrowserNetworkAdapter, createOpfsFileSystem, InMemoryFileSystem, } from "@secure-exec/browser";
|
|
3
|
+
export type { ExecOptions, ExecResult, NodeRuntimeDriver, StdioChannel, StdioEvent, TimingMitigation, } from "@secure-exec/browser";
|
|
4
|
+
export { allowAll, allowAllChildProcess, allowAllEnv, allowAllFs, allowAllNetwork, createInMemoryFileSystem, } from "@secure-exec/browser";
|
|
5
|
+
export type { BrowserRuntimeDriverFactoryOptions, ConvergedSidecarFactoryOptions, ConvergedSidecarHandle, } from "@secure-exec/browser";
|
|
6
|
+
export { createBrowserRuntimeDriverFactory } from "@secure-exec/browser";
|
|
7
|
+
export type { WorkerHandle } from "@secure-exec/browser";
|
|
8
|
+
export { BrowserWorkerAdapter } from "@secure-exec/browser";
|
|
9
|
+
export type { AgentOsConvergedSidecarOptions } from "./converged-sidecar.js";
|
|
10
|
+
export { createAgentOsConvergedSidecar } from "./converged-sidecar.js";
|
|
11
|
+
export type { ConvergedExecutionHostBridge } from "./converged-execution-host-bridge.js";
|
|
12
|
+
export { createConvergedExecutionHostBridge } from "./converged-execution-host-bridge.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// @rivet-dev/agentos-browser — converged browser runtime for Agent OS.
|
|
2
|
+
//
|
|
3
|
+
// The browser runtime is @secure-exec/browser's CONVERGED stack (worker,
|
|
4
|
+
// SharedArrayBuffer sync-bridge, fs/net/dns/module servicers, all enforced by the
|
|
5
|
+
// wasm kernel). Agent OS does not carry its own copy; it re-exports that runtime
|
|
6
|
+
// and adds only the ACP/wasm-sidecar layer (createAgentOsConvergedSidecar). The
|
|
7
|
+
// pre-convergence TS-kernel files (worker/runtime/driver/sync-bridge/permission
|
|
8
|
+
// eval) were deleted in the reconciliation — the kernel is the sole enforcement
|
|
9
|
+
// point, so guest-side permission eval no longer exists here.
|
|
10
|
+
//
|
|
11
|
+
// Per the converged model (kernel-owns-fs), per-runtime OPFS *namespace* helpers
|
|
12
|
+
// (listOpfsNamespaces/releaseOpfsNamespace) are gone: storage isolation is the
|
|
13
|
+
// kernel's responsibility, not a TS-layer concern.
|
|
14
|
+
export { createBrowserDriver, createBrowserNetworkAdapter, createOpfsFileSystem, InMemoryFileSystem, } from "@secure-exec/browser";
|
|
15
|
+
export { allowAll, allowAllChildProcess, allowAllEnv, allowAllFs, allowAllNetwork, createInMemoryFileSystem, } from "@secure-exec/browser";
|
|
16
|
+
export { createBrowserRuntimeDriverFactory } from "@secure-exec/browser";
|
|
17
|
+
export { BrowserWorkerAdapter } from "@secure-exec/browser";
|
|
18
|
+
export { createAgentOsConvergedSidecar } from "./converged-sidecar.js";
|
|
19
|
+
export { createConvergedExecutionHostBridge } from "./converged-execution-host-bridge.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface HttpRequest {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
headers: Map<string, string>;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HttpResponse {
|
|
8
|
+
status: number;
|
|
9
|
+
headers: Map<string, string>;
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
/** Build a minimal HTTP/1.1 request (the client/pi side). */
|
|
13
|
+
export declare function buildHttpRequest(method: string, path: string, body: string, host?: string): Uint8Array;
|
|
14
|
+
/** Build a minimal HTTP/1.1 response (the proxy side). */
|
|
15
|
+
export declare function buildHttpResponse(status: number, body: string): Uint8Array;
|
|
16
|
+
/** Parse a complete HTTP request, reading more via `readChunk` until the body is in. */
|
|
17
|
+
export declare function readHttpRequest(initial: Uint8Array, readChunk: () => Uint8Array | null): HttpRequest | null;
|
|
18
|
+
/** Parse a complete HTTP response (the client/pi side). */
|
|
19
|
+
export declare function readHttpResponse(initial: Uint8Array, readChunk: () => Uint8Array | null): HttpResponse | null;
|
|
20
|
+
/** The proxy core: turn one parsed HTTP request into an HTTP response by forwarding
|
|
21
|
+
* the body to `infer` (the host.inference call). Only POSTs to a chat/completions or
|
|
22
|
+
* /v1/messages path are forwarded; anything else is a 404-shaped error. */
|
|
23
|
+
export declare function handleProxyRequest(request: HttpRequest, infer: (body: string) => Promise<string> | string): Promise<Uint8Array>;
|