Networking and native services
App JavaScript owns provider mapping and domain semantics. The native host owns credential application, TLS, exact endpoint/operation policy, response bounds and the Action deadline.
HTTP: declare capability and exact policy
{
"capabilities": ["data.sqlite", "net.http"],
"nativeServices": {
"http": [
{
"method": "POST",
"urls": [
"https://api.example.com/search",
"https://api.example.com/contents"
],
"allowedRequestHeaders": ["accept", "content-type"],
"credential": {
"id": "example.api-key",
"header": "authorization",
"prefix": "Bearer "
}
}
]
}
}The App declares where it may connect, which method it may use, which request headers its JavaScript may set and which native credential binding should be applied. The secret value is supplied only during first-install transport and is stored by the host.
Call with the installed fetch()
async function post(path, value) {
const response = await fetch(`https://api.example.com/${path}`, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
},
body: JSON.stringify(value),
timeoutMs: PocketPi.actionContext.remainingMs(),
maxBytes: 96 * 1024,
});
const body = await response.json();
if (!response.ok) {
throw new Error(`Provider HTTP ${response.status}: ${JSON.stringify(body)}`);
}
return body;
}The response provides status, url, frozen headers, ok, and async bytes(), arrayBuffer(), text() andjson(). Always set a product-appropriate maxBytes rather than relying on the default 128 KiB.
MCP: keep the connection native and the Tool policy App-owned
{
"nativeServices": {
"mcp": [
{
"connection": "portfolio",
"url": "https://provider.example.com/mcp",
"credential": {
"id": "portfolio.oauth-token",
"header": "authorization",
"prefix": "Bearer "
}
}
]
},
"providerOperations": ["get_accounts", "get_portfolio"]
}const value = PocketPi.services.call("mcp.client", "callTool", {
connection: "portfolio",
name: "get_portfolio",
arguments: { account_number: account },
retryable: true,
});providerOperations is the native allowlist. App source still owns which public Tools exist, how arguments are validated, which upstream operation is selected, how the response maps into product Data, and what the Agent receives.
Batch only when the product needs it
const value = PocketPi.services.call("mcp.client", "callTools", {
connection: "portfolio",
calls: [
{ name: "get_accounts", arguments: {} },
{ name: "get_portfolio", arguments: { account_number: account } },
],
retryable: true,
});A batch shares the same Action deadline and native connection policy. It does not turn native code into the product workflow; JavaScript still decides the calls and consumes the results.
Credential file for first install
{
"example.api-key": "secret value supplied out of band"
}cargo xtask package app example path/to/credentials.jsonThe credential ids must exactly match the manifest. The Installer removes the file from the staged source and stores values natively. Update packages omit credentials and may not change native permission policy.
Rules that keep the boundary honest
- Never put a secret in
app.json, an asset, SQLite, Tool arguments or Agent workspace. - Use exact URLs and operations; do not declare a broad proxy and rebuild authorization in JavaScript.
- Validate domain arguments before crossing the native service boundary.
- Return useful live results to the Agent; persist only bounded View/product state.
- Treat ambiguous real-world side effects as unknown, not as safe to retry with a new id.