Portable Agent Packages
Portable Agent Packages
exportAgentPackage() captures the serializable state of an already-resolved
native Agent: its unrendered instructions, portable runtime limits, context
state, tool definitions and placement, plus optional Realtime metadata. It
never serializes executable functions or connector credentials.
import {
AgentPackageToolServer,
createPortableToolImplementationFingerprint,
exportAgentPackage,
hydrateAgentPackage,
} from 'oneringai';
const localToolFingerprints = new Map([
['read_desktop_file', createPortableToolImplementationFingerprint(
readDesktopFile.definition,
'desktop:read_desktop_file:v3',
)],
]);
// Trusted source server
const packageValue = exportAgentPackage(serverAgent, {
expiresAt: new Date(Date.now() + 10 * 60_000),
toolPlacement: (name) => name === 'read_desktop_file' ? 'local' : 'remote',
toolImplementationFingerprint: (name) => localToolFingerprints.get(name),
realtime: {
provider: 'openai',
connectorName: 'server-openai',
model: 'gpt-realtime-2.1',
voice: 'marin',
},
});
const toolServer = new AgentPackageToolServer(serverAgent, packageValue);
// Trusted desktop main process
const desktopAgent = await hydrateAgentPackage(packageValue, {
connector: 'desktop-openai',
model: 'gpt-5.6-terra',
userId: authenticatedUser.id,
permissions: desktopPermissionPolicy,
contextFactory: ({ package: pkg, model, userId, identities }) => ({
model,
agentId: pkg.agent.id,
systemPrompt: pkg.agent.instructions,
userId,
identities,
features: trustedContextFeatures,
plugins: trustedContextPlugins,
}),
localToolResolver: (descriptor) => {
const tool = resolveAuthorizedDesktopTool(descriptor);
const implementationFingerprint = localToolFingerprints.get(
descriptor.definition.function.name,
);
return tool && implementationFingerprint
? { tool, implementationFingerprint }
: undefined;
},
remoteToolTransport: {
execute: (request) => authenticatedRpc.executeRemoteTool(request),
},
agentConfig: trustedProviderPolicy,
});
Custom tools placed local must use a stable implementation ID that both
trusted runtimes map through createPortableToolImplementationFingerprint().
The implementation ID is required and must change whenever executable behavior
or one of its dependencies changes.
Hydration requires localToolResolver to return a ResolvedLocalTool; returning
a bare ToolFunction is rejected. Generated OneRingAI built-ins use their
runtime-aware registry fingerprints automatically and can be wrapped with
createResolvedLocalTool(tool) on the receiving host. A matching name or JSON
schema alone is never treated as executable compatibility.
Fingerprint inputs follow JSON-wire semantics, so omitted and explicitly
undefined optional fields do not diverge after serialization. A tool's
dynamic function description is prompt presentation generated independently on
each host and is excluded from its local executable contract; its callable
schema, blocking mode, and timeout still must match. Remote tool definitions
retain their full canonical definition fingerprint, which package validation
recomputes before hydration or tool-server construction.
The RPC host must authenticate and authorize each request before forwarding it
to toolServer.execute(request). Hydration also rejects a package tool whose
name collides with a trusted context, identity, or host tool; package tools
never overwrite host authority.
Packaged descriptor.permission values are informational and are never applied
directly. If a portable tool needs a receiving-host override, provide
toolPermissionResolver: (descriptor) => trustedPolicyFor(descriptor) to
hydrateAgentPackage(). Returning undefined leaves the tool without a
per-tool override; the Agent's required host-owned permission system still
governs execution.
The package and remote-tool protocol require exact JSON objects. A package is limited to 10,000,000 UTF-8 bytes. Remote arguments and successful results are limited to 1,000,000 UTF-8 bytes each, and one tool-server session accepts at most 1,000 distinct request IDs. Unknown fields, non-JSON values, expired or incompatible packages, cross-package requests, and non-allowlisted tools fail closed. Exact request retries are idempotent for the bounded server-session lifetime.
Agent packages and remote-tool DTOs use exact protocol version 2. Version 1 peers reject version 2 objects, and version 2 peers reject version 1 objects. Deploy the exporting server and receiving host together, or keep the old pair active until all in-flight version 1 sessions have ended. Do not mix protocol versions within one tool-server session.
Call await toolServer.close() and desktopAgent.destroy() when the session
ends. Concurrent close callers share the same shutdown operation, so every
caller waits for in-flight tool execution to settle.
For a voice package, pass executionProfile: 'realtime' during hydration.
The package's connector and model remain hints; an authorized
connectorResolver may replace both. See
Distributed Agent Execution
for ownership, Electron/WebRTC flow, and security boundaries.