nanocodex 0.6.0 → 0.6.4

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.
Files changed (54) hide show
  1. package/README.md +158 -6
  2. package/browser/InlineAgent.mjs +2 -0
  3. package/browser/Transport.d.mts +3 -0
  4. package/browser/Transport.mjs +1 -0
  5. package/browser/Voice.d.mts +2 -0
  6. package/browser/Voice.mjs +12 -6
  7. package/browser/VoiceSession.mjs +71 -21
  8. package/browser/WorkerAgent.mjs +2 -1
  9. package/browser/host.d.mts +7 -0
  10. package/browser/host.mjs +19 -0
  11. package/cloud/Decorator.d.mts +9 -0
  12. package/cloud/Decorator.mjs +9 -0
  13. package/cloud/Transport.mjs +6 -0
  14. package/cloud/actions/connection.mjs +3 -0
  15. package/cloud/actions/connector.d.mts +18 -0
  16. package/cloud/actions/connector.mjs +22 -0
  17. package/cloud/actions/index.d.mts +2 -0
  18. package/cloud/actions/index.mjs +2 -0
  19. package/cloud/internal.mjs +5 -0
  20. package/cloud/types.d.mts +2 -0
  21. package/cloudflare/Agent.mjs +7 -1
  22. package/cloudflare/egress.d.mts +3 -1
  23. package/cloudflare/egress.mjs +14 -0
  24. package/cloudflare/managed-access.d.mts +23 -0
  25. package/cloudflare/managed-access.mjs +120 -0
  26. package/internal.mjs +27 -0
  27. package/managed/Access.d.mts +5 -0
  28. package/managed/Access.mjs +66 -0
  29. package/managed/Agent.d.mts +16 -7
  30. package/managed/Agent.mjs +28 -7
  31. package/managed/Voice.mjs +1 -0
  32. package/managed/index.d.mts +1 -0
  33. package/managed/index.mjs +1 -0
  34. package/node/host.mjs +12 -0
  35. package/package.json +11 -2
  36. package/pkg-node/nanocodex.js +55 -40
  37. package/pkg-node/nanocodex_bg.wasm.d.ts +3 -3
  38. package/pkg-web/.nanocodex-bindgen-stamp +1 -1
  39. package/pkg-web/nanocodex-build.json +1 -1
  40. package/pkg-web/nanocodex.d.ts +3 -3
  41. package/pkg-web/nanocodex.js +55 -40
  42. package/pkg-web/nanocodex_bg.js +55 -40
  43. package/pkg-web/nanocodex_bg.wasm +0 -0
  44. package/pkg-web/nanocodex_bg.wasm.d.ts +3 -3
  45. package/runtime/code-evaluator.worker.mjs +28 -22
  46. package/runtime/managed-transport.mjs +1 -0
  47. package/runtime/quickjs-evaluator.mjs +59 -17
  48. package/runtime/response-controls.mjs +41 -24
  49. package/runtime/responses-http.mjs +69 -0
  50. package/tools/browser/accountInfo.mjs +53 -8
  51. package/tools/browser/browserShell.mjs +2 -2
  52. package/tools/browser/index.mjs +3 -3
  53. package/tools/environment.d.mts +34 -0
  54. package/tools/environment.mjs +59 -0
@@ -0,0 +1,22 @@
1
+ import { API_CONNECTORS } from "../internal.mjs";
2
+
3
+ const SERVICES = new Set(API_CONNECTORS);
4
+
5
+ /** Calls an opted-in service using the current app grant, without an agent turn. */
6
+ export async function request(client, options) {
7
+ if (!SERVICES.has(options?.connector)) throw new TypeError("Unknown connector API");
8
+ if (typeof options.path !== "string" || !options.path.startsWith("/") || options.path.startsWith("//")) {
9
+ throw new TypeError("Connector requests require a provider-relative path");
10
+ }
11
+ return client.fetch(`/v1/connectors/${options.connector}/request`, {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json", origin: client.appOrigin },
14
+ body: JSON.stringify({
15
+ path: options.path,
16
+ method: options.method ?? "GET",
17
+ ...(options.connectionId === undefined ? {} : { connection_id: options.connectionId }),
18
+ ...(options.body === undefined ? {} : { body: options.body }),
19
+ }),
20
+ signal: options.signal,
21
+ });
22
+ }
@@ -1,5 +1,7 @@
1
1
  export * as account from "./account.mjs";
2
2
  export * as agent from "./agent.mjs";
3
+ export * as connector from "./connector.mjs";
4
+ export * as connectors from "./connector.mjs";
3
5
  export * as connection from "./connection.mjs";
4
6
  export * as grant from "./grant.mjs";
5
7
  export * as machineUsd from "./machineUsd.mjs";
@@ -1,5 +1,7 @@
1
1
  export * as account from "./account.mjs";
2
2
  export * as agent from "./agent.mjs";
3
+ export * as connector from "./connector.mjs";
4
+ export * as connectors from "./connector.mjs";
3
5
  export * as connection from "./connection.mjs";
4
6
  export * as grant from "./grant.mjs";
5
7
  export * as machineUsd from "./machineUsd.mjs";
@@ -12,8 +12,13 @@ const CLOUD_ACCOUNT_PROVIDERS = Object.freeze([
12
12
  "gcontacts",
13
13
  "slack",
14
14
  "x",
15
+ "spotify",
16
+ "soundcloud",
17
+ "link",
15
18
  "chatgpt",
16
19
  ]);
20
+ export const API_CONNECTORS = Object.freeze(CLOUD_ACCOUNT_PROVIDERS.filter(provider => provider !== "chatgpt"));
21
+
17
22
  const MCP_CONNECTION_ID = /^[A-Za-z0-9_-]{43}$/;
18
23
  const AGENT_CONVERSATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
19
24
  const CATALOG_DIGEST = /^0x[0-9a-f]{64}$/;
package/cloud/types.d.mts CHANGED
@@ -12,6 +12,8 @@ export type CloudAccount =
12
12
  | "gcontacts"
13
13
  | "slack"
14
14
  | "x"
15
+ | "spotify"
16
+ | "soundcloud" | "link"
15
17
  | "chatgpt";
16
18
 
17
19
  /** Secret-free account identity returned by a connector status reader. */
@@ -1,4 +1,4 @@
1
- import { responseControlsSocket } from "../runtime/response-controls.mjs";
1
+ import { responseControlsBody, responseControlsSocket } from "../runtime/response-controls.mjs";
2
2
  import * as HostAgent from "../host/Agent.mjs";
3
3
  import {
4
4
  CLOUDFLARE_SESSION_RESERVATION,
@@ -316,6 +316,12 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
316
316
  const transport = Transport.hostManaged({
317
317
  ...endpoint,
318
318
  websocketPreconnect: true,
319
+ createResponse(url, id, request) {
320
+ return endpoint.createResponse(url, id, {
321
+ ...request,
322
+ body: responseControlsBody(request.body, internalRuntime?.responseControls),
323
+ });
324
+ },
319
325
  async createWebSocket(url, id, request) {
320
326
  try {
321
327
  const opened = await endpoint.createWebSocket(url, id, request);
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BrowserHttpRequest,
2
3
  BrowserWebSocketConnection,
3
4
  BrowserWebSocketRequest,
4
5
  } from "../browser/host.mjs";
@@ -23,6 +24,7 @@ export type CloudflareEgressOptions = Readonly<{
23
24
  export type CloudflareEgressTransportOptions = Readonly<{
24
25
  apiBaseUrl: string;
25
26
  websocketUrl: string;
27
+ createResponse(endpoint: string, sessionId: string, request: BrowserHttpRequest): Promise<Response>;
26
28
  createWebSocket(
27
29
  endpoint: string,
28
30
  sessionId: string,
@@ -31,7 +33,7 @@ export type CloudflareEgressTransportOptions = Readonly<{
31
33
  }>;
32
34
 
33
35
  /**
34
- * Creates the brokered WebSocket seam for a managed Cloudflare Worker.
36
+ * Creates the brokered WebSocket and streaming HTTPS seams for a managed Cloudflare Worker.
35
37
  *
36
38
  * The managed Worker supplies only fixed placeholders to `binding`; the
37
39
  * separately deployed broker owns and injects the real provider credential.
@@ -1,3 +1,4 @@
1
+ import { responsesHttpHeaders } from "../runtime/responses-http.mjs";
1
2
  import { cloudflareEgressSubject } from "./egress-subject.mjs";
2
3
 
3
4
  const OPENAI_WEBSOCKET_BETA = "responses_websockets=2026-02-06";
@@ -14,6 +15,19 @@ export function cloudflareEgress(options) {
14
15
  return Object.freeze({
15
16
  apiBaseUrl: BROKER_API_BASE_URL,
16
17
  websocketUrl: BROKER_WEBSOCKET_URL,
18
+ async createResponse(endpoint, sessionId, request) {
19
+ if (request?.authorization !== "host_managed") {
20
+ throw new Error("Cloudflare EGRESS requires Transport.hostManaged authorization");
21
+ }
22
+ const url = exactWebSocketEndpoint(endpoint, BROKER_WEBSOCKET_URL);
23
+ const headers = responsesHttpHeaders("NANOCODEX_PROVIDER_CREDENTIAL", sessionId, {
24
+ threadId: request.threadId, turnState: request.turnState,
25
+ });
26
+ const subject = cloudflareEgressSubject(binding);
27
+ if (subject !== undefined) headers.set("x-nanocodex-subject", subject);
28
+ return binding.fetch(url, { method: "POST", headers, body: request.body,
29
+ signal: request.signal, redirect: "manual" });
30
+ },
17
31
  createWebSocket: (endpoint, sessionId, request) =>
18
32
  openBrokeredWebSocket(binding, endpoint, sessionId, request),
19
33
  });
@@ -0,0 +1,23 @@
1
+ /** Credential-bound authority reused for at most 120 seconds; live renewal is separate. */
2
+ export type ManagedAccessPrincipal = Readonly<{
3
+ kind: "account_session" | "api_key" | "connect_grant" | "service";
4
+ userId: string;
5
+ organizationId: string;
6
+ teamId: string;
7
+ authorizationEpoch: number;
8
+ capabilities: readonly string[];
9
+ connectGrant?: unknown;
10
+ }>;
11
+ export type ManagedAccessEnv = { NANOCODEX_ACCESS_SECRET?: string; DEPLOYMENT_SHA?: string };
12
+ export type ManagedAccessClaims<P extends ManagedAccessPrincipal = ManagedAccessPrincipal> = {
13
+ version: 1; audience: string; binding: string; issuedAt: number; expiresAt: number; principal: P;
14
+ };
15
+ export const MANAGED_ACCESS_HEADER: "x-nanocodex-access";
16
+ export const MANAGED_ACCESS_TTL_MS: 120000;
17
+ export function managedAccessRequest(request: Request): boolean;
18
+ export function readManagedAccess<P extends ManagedAccessPrincipal = ManagedAccessPrincipal>(request: Request, env: ManagedAccessEnv, now?: number): Promise<P | undefined>;
19
+ export function createManagedAccessClaims<P extends ManagedAccessPrincipal>(request: Request, principal: P, now?: number): Promise<ManagedAccessClaims<P>>;
20
+ export function signManagedAccessClaims(claims: ManagedAccessClaims, env: ManagedAccessEnv): Promise<string>;
21
+ export function isHandViewerUpgrade(request: Request): boolean;
22
+ export function handRequestFailure(request: Request, principal: ManagedAccessPrincipal): "forbidden" | "forbidden_origin" | undefined;
23
+ export function handBrokerRequest(request: Request, principal: ManagedAccessPrincipal): Request;
@@ -0,0 +1,120 @@
1
+ export const MANAGED_ACCESS_HEADER = "x-nanocodex-access";
2
+ export const MANAGED_ACCESS_TTL_MS = 120_000;
3
+ const encoder = new TextEncoder();
4
+ let cachedKey;
5
+
6
+ /** Finite agent operations and screen viewer admission. Publishers, renewal and
7
+ * agent streams retain live authentication; a viewer renews live every 10 s. */
8
+ export function managedAccessRequest(request) {
9
+ const path = new URL(request.url).pathname;
10
+ if (path === "/v1/account/hands/view") {
11
+ return request.method === "GET" && request.headers.get("upgrade")?.toLowerCase() === "websocket";
12
+ }
13
+ if (path === "/v1/account/hands/screens" || path === "/v1/account/hands/ice") {
14
+ return request.method === (path.endsWith("/screens") ? "GET" : "POST")
15
+ && !request.headers.has("upgrade");
16
+ }
17
+ return /^\/v1\/agents(?:\/|$)/.test(path)
18
+ && !request.headers.has("upgrade")
19
+ && !request.headers.get("accept")?.includes("text/event-stream")
20
+ && !/\/(?:ws|events|tool-host|device-host|sideband)$/.test(path);
21
+ }
22
+
23
+ function key(secret) {
24
+ if (secret.length < 32) throw new Error("managed_access_not_configured");
25
+ if (cachedKey?.secret !== secret) cachedKey = { secret, promise: crypto.subtle.importKey(
26
+ "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"],
27
+ ) };
28
+ return cachedKey.promise;
29
+ }
30
+ function encode(bytes) {
31
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
32
+ }
33
+ function decode(value) {
34
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("invalid_managed_access");
35
+ return Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/")), c => c.charCodeAt(0));
36
+ }
37
+
38
+ /** Bind reuse to the original login/key/grant, including all Connect restrictions. */
39
+ async function credentialBinding(request, kind) {
40
+ const url = new URL(request.url);
41
+ const connect = kind === "connect_grant" && url.origin === "https://nanocodex.internal" && request.headers.has("x-nanocodex-connect-user");
42
+ // Match the live authenticator's first-cookie and whitespace semantics.
43
+ let cookie;
44
+ for (const part of request.headers.get("cookie")?.split(";") ?? []) {
45
+ const separator = part.indexOf("=");
46
+ if (separator >= 0 && part.slice(0, separator).trim() === "nanocodex_account") {
47
+ cookie = part.slice(separator + 1).trim();
48
+ break;
49
+ }
50
+ }
51
+ const authorization = request.headers.get("authorization");
52
+ if (!(connect || (kind === "account_session" && cookie) || (kind === "api_key" && authorization))) throw new Error("managed_access_requires_session");
53
+ // Include every authority input so adding a cookie/Connect restriction cannot
54
+ // make a cached API-key principal override the live authenticator's precedence.
55
+ const identity = JSON.stringify({ kind, authorization, cookie: cookie ?? null,
56
+ connect: [...request.headers].filter(([name]) => name.startsWith("x-nanocodex-connect-")).sort() });
57
+ return encode(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(identity))));
58
+ }
59
+
60
+ export async function readManagedAccess(request, env, now = Date.now()) {
61
+ try {
62
+ if (!managedAccessRequest(request)) return;
63
+ const token = request.headers.get(MANAGED_ACCESS_HEADER);
64
+ if (!token || token.length > 16_384) return;
65
+ const [prefix, payload, signature, extra] = token.split(".");
66
+ if (prefix !== "ncx_access_v1" || !payload || !signature || extra !== undefined) return;
67
+ if (!await crypto.subtle.verify("HMAC", await key(env.NANOCODEX_ACCESS_SECRET ?? ""), decode(signature), encoder.encode(`${prefix}.${payload}`))) return;
68
+ const value = JSON.parse(new TextDecoder().decode(decode(payload)));
69
+ if (value.version !== 1 || value.audience !== new URL(request.url).origin
70
+ || !Number.isSafeInteger(value.issuedAt) || !Number.isSafeInteger(value.expiresAt)
71
+ || value.issuedAt > now || value.expiresAt <= now || value.expiresAt <= value.issuedAt
72
+ || value.expiresAt - value.issuedAt > MANAGED_ACCESS_TTL_MS
73
+ || !value.principal || !["account_session", "api_key", "connect_grant"].includes(value.principal.kind)
74
+ || value.binding !== await credentialBinding(request, value.principal.kind)) return;
75
+ return value.principal;
76
+ } catch { return; }
77
+ }
78
+
79
+ /** Shared issuance keeps the exact audience, credential binding and lifetime wire contract. */
80
+ export async function createManagedAccessClaims(request, principal, now = Date.now()) {
81
+ return { version: 1, audience: new URL(request.url).origin, binding: await credentialBinding(request, principal.kind),
82
+ issuedAt: now, expiresAt: now + MANAGED_ACCESS_TTL_MS, principal };
83
+ }
84
+ export async function signManagedAccessClaims(claims, env) {
85
+ const payload = `ncx_access_v1.${encode(encoder.encode(JSON.stringify(claims)))}`;
86
+ const signature = await crypto.subtle.sign("HMAC", await key(env.NANOCODEX_ACCESS_SECRET ?? ""), encoder.encode(payload));
87
+ return `${payload}.${encode(new Uint8Array(signature))}`;
88
+ }
89
+
90
+ export function isHandViewerUpgrade(request) {
91
+ return request.method === "GET" && new URL(request.url).pathname === "/v1/account/hands/view"
92
+ && request.headers.get("upgrade")?.toLowerCase() === "websocket";
93
+ }
94
+
95
+ /** Shared account Hand policy, applied after live or signed-snapshot authentication. */
96
+ export function handRequestFailure(request, principal) {
97
+ const url = new URL(request.url);
98
+ if (principal.connectGrant || !principal.capabilities.includes("agents:read")
99
+ || !principal.capabilities.includes("tools:use")
100
+ || (url.pathname.endsWith("/host") && !principal.capabilities.includes("agents:write"))) return "forbidden";
101
+ if (principal.kind !== "api_key" && (request.method !== "GET" || request.headers.has("upgrade"))
102
+ && request.headers.get("origin") !== url.origin) return "forbidden_origin";
103
+ }
104
+
105
+ /** Only for an authenticated principal that passed handRequestFailure. */
106
+ export function handBrokerRequest(request, principal) {
107
+ const url = new URL(request.url);
108
+ const headers = new Headers(request.headers);
109
+ headers.delete("x-nanocodex-remote-vm");
110
+ headers.set("x-nanocodex-owner-id", principal.userId);
111
+ headers.set("x-nanocodex-session-organization-id", principal.organizationId);
112
+ headers.set("x-nanocodex-session-team-id", principal.teamId);
113
+ headers.set("x-nanocodex-authorization-epoch", String(principal.authorizationEpoch));
114
+ headers.set("x-nanocodex-capabilities", JSON.stringify(principal.capabilities));
115
+ for (const name of ["x-nanocodex-connect-user", "x-nanocodex-connect-grant-id", "x-nanocodex-connect-capabilities",
116
+ "x-nanocodex-connect-connectors", "x-nanocodex-connect-connector-connections", "x-nanocodex-connect-mcp-ids",
117
+ "x-nanocodex-connect-app-tool-catalog-digest"]) headers.delete(name);
118
+ return new Request(`https://account-tools.internal${url.pathname.slice("/v1/account".length)}${url.search}`,
119
+ new Request(request, { headers }));
120
+ }
package/internal.mjs CHANGED
@@ -12,6 +12,7 @@ const activeAgentSessions = new Map();
12
12
  const pendingCloudflareAgentSessions = new Map();
13
13
  const cloudflareAgentSessions = new WeakSet();
14
14
  const hostConnections = new Map();
15
+ const hostHttpRequests = new Map();
15
16
  const definitionHosts = new Map();
16
17
  let nextHostConnection = 1;
17
18
  let nextDefinitionHost = 1;
@@ -445,6 +446,32 @@ export function releaseDefinitionHost(id) {
445
446
  }
446
447
 
447
448
  const hostBridge = Object.freeze({
449
+ httpOpen(endpoint, apiKey, accountId, fedramp, sessionId, threadId, turnState, body) {
450
+ const host = requiredSessionHost(threadId);
451
+ if (typeof host.httpOpen !== "function") {
452
+ throw JSON.stringify({ kind: "transport", detail: "host HTTPS transport is unavailable", reconnectable: false });
453
+ }
454
+ const local = host.httpOpen(endpoint, apiKey, sessionId, { accountId, fedramp, threadId, turnState }, body);
455
+ const handle = nextHostConnection++;
456
+ hostHttpRequests.set(handle, { host, handle: local });
457
+ return handle;
458
+ },
459
+ async httpReady(handle) {
460
+ const request = hostHttpRequests.get(handle);
461
+ try { return await request.host.httpReady(request.handle); }
462
+ catch (error) { throw JSON.stringify(connectFailure(error)); }
463
+ },
464
+ async httpNext(handle) {
465
+ const request = hostHttpRequests.get(handle);
466
+ try { return await request.host.httpNext(request.handle); }
467
+ catch (error) { throw JSON.stringify(connectFailure(error)); }
468
+ },
469
+ httpClose(handle) {
470
+ const request = hostHttpRequests.get(handle);
471
+ if (!request) return;
472
+ hostHttpRequests.delete(handle);
473
+ request.host.httpClose(request.handle);
474
+ },
448
475
  async connect(endpoint, apiKey, accountId, fedramp, sessionId, threadId, turnState) {
449
476
  const host = requiredSessionHost(threadId);
450
477
  let result;
@@ -0,0 +1,5 @@
1
+ /** Reuses short-lived server-issued authorization for finite managed Agent HTTP requests.
2
+ * Retains the original credential and scopes; retries once only on explicit pre-admission rejection.
3
+ * Cache state is bounded, in memory, and separated by origin and credential/grant.
4
+ */
5
+ export function withManagedAccess(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
@@ -0,0 +1,66 @@
1
+ const stores = new WeakMap();
2
+ const HEADER = "x-nanocodex-access";
3
+
4
+ /** Reuse finite-request authority across Agent handles for the same client identity. */
5
+ export function managedAccessFetch(fetchImpl, origin, identity = "browser") {
6
+ let store = stores.get(fetchImpl);
7
+ if (!store) stores.set(fetchImpl, store = new Map());
8
+ const key = `${origin}\n${identity}`;
9
+ let state = store.get(key);
10
+ if (!state) {
11
+ if (store.size >= 128) store.delete(store.keys().next().value);
12
+ store.set(key, state = {});
13
+ }
14
+ const clock = () => performance.now();
15
+ function remember(response, began) {
16
+ const token = response.headers.get(HEADER);
17
+ const ttl = Number(response.headers.get("x-nanocodex-access-ttl-ms"));
18
+ if (response.ok && token?.startsWith("ncx_access_v1.") && token.length <= 16_384
19
+ && Number.isFinite(ttl) && ttl > 0 && ttl <= 120_000) {
20
+ const until = began + ttl;
21
+ if (!state.until || until > state.until) { state.token = token; state.until = until; }
22
+ }
23
+ }
24
+ return async (url, init = {}) => {
25
+ const source = url instanceof Request ? url : undefined;
26
+ const target = new URL(source?.url ?? url);
27
+ const headers = new Headers(source?.headers ?? init.headers);
28
+ headers.delete(HEADER);
29
+ const eligible = target.origin === origin && /^\/v1\/agents(?:\/|$)/.test(target.pathname)
30
+ && !headers.has("upgrade") && !headers.get("accept")?.includes("text/event-stream")
31
+ && !/\/(?:ws|events|tool-host|device-host|sideband)$/.test(target.pathname);
32
+ const token = eligible && state.until > clock() + 5000 ? state.token : undefined;
33
+ if (token) headers.set(HEADER, token);
34
+ const began = clock();
35
+ const retryRequest = source && token ? source.clone() : undefined;
36
+ let response;
37
+ try {
38
+ response = source ? await fetchImpl(new Request(source, { headers })) : await fetchImpl(url, { ...init, headers });
39
+ } catch (error) {
40
+ if (retryRequest?.body) void retryRequest.body.cancel().catch(() => {});
41
+ throw error;
42
+ }
43
+ if (token && response.status === 401 && response.headers.get("x-nanocodex-access-rejected") === "1") {
44
+ await response.body?.cancel();
45
+ if (state.token === token) { state.token = undefined; state.until = undefined; }
46
+ const retryHeaders = new Headers(headers);
47
+ retryHeaders.delete(HEADER);
48
+ // Rejected by ingress before admission. Preserve the exact body and idempotency key.
49
+ response = retryRequest ? await fetchImpl(new Request(retryRequest, { headers: retryHeaders }))
50
+ : await fetchImpl(url, { ...init, headers: retryHeaders });
51
+ }
52
+ if (retryRequest?.body && !retryRequest.bodyUsed) void retryRequest.body.cancel().catch(() => {});
53
+ remember(response, began);
54
+ return response;
55
+ };
56
+ }
57
+
58
+ /** Wrap a managed HTTP transport with bounded, credential-bound access reuse. */
59
+ export function withManagedAccess(fetchImpl) {
60
+ return (input, init) => {
61
+ const request = new Request(input, init);
62
+ const identity = JSON.stringify([...request.headers].filter(([name]) =>
63
+ name === "authorization" || name === "cookie" || name.startsWith("x-nanocodex-connect-")).sort());
64
+ return managedAccessFetch(fetchImpl, new URL(request.url).origin, identity)(request);
65
+ };
66
+ }
@@ -1,3 +1,4 @@
1
+ import type { RequestOriginContext } from "../tools/environment.mjs";
1
2
  import type { Model, PromptInput, ReasoningMode, Thinking, TurnUsage } from "../types.mjs";
2
3
  import type { AgentId } from "../runtime/subagents.mjs";
3
4
 
@@ -106,6 +107,8 @@ export type Organization = Readonly<{
106
107
  export type OrganizationUpdate = Readonly<{ name: string | null }>;
107
108
 
108
109
  export type Options = Readonly<{
110
+ /** Descriptive client-reported context; never grants authority or changes execution placement. */
111
+ requestOrigin?: RequestOriginContext;
109
112
  /** Managed service origin. Defaults to the current browser origin. */
110
113
  baseUrl?: string | URL | undefined;
111
114
  /** Server credential. Browsers omit this and authenticate with the account cookie. */
@@ -141,6 +144,8 @@ export type Environment = Readonly<{
141
144
  network?: NetworkPolicy;
142
145
  }>;
143
146
  export type Configuration = Readonly<{
147
+ /** Pin this session to one connected ChatGPT account; disables automatic account failover. */
148
+ chatgpt_account_id?: string;
144
149
  settings?: Readonly<{ model: CreateSettings["model"]; thinking: CreateSettings["thinking"]; reasoning_mode: "standard" | "pro"; fast_mode: boolean }>;
145
150
  instructions?: string;
146
151
  tools?: readonly string[];
@@ -390,6 +395,9 @@ export type CronTrigger = Readonly<{
390
395
  }>;
391
396
 
392
397
  export type Agent = Readonly<{
398
+ /** Start bounded background runtime/socket preparation for an active conversation.
399
+ * Resolves on acceptance, not provider readiness. Never required before prompt(). */
400
+ prepare(options?: Readonly<{ signal?: AbortSignal }>): Promise<void>;
393
401
  requiredActions: Readonly<{
394
402
  list(): Promise<{ data: readonly Readonly<{ call_id: string; session_id: string; source_call_id: string; name: string; input: unknown; deadline_at: number }>[] }>;
395
403
  submit(callId: string, outcome: import("nanocodex-tools/hosted").HostedToolCallOutcome): Promise<void>;
@@ -443,15 +451,16 @@ export function remove(id: string, options?: Options): Promise<void>;
443
451
  export { remove as delete };
444
452
  export function findSessions(request: FindSessionsRequest, options?: Options): Promise<FindSessionsResponse>;
445
453
  export function readSession(request: ReadSessionRequest, options?: Options): Promise<ReadSessionResponse>;
454
+ export type MemoryOptions = Options & Readonly<{ scope?: "team" | "personal" }>;
446
455
  /** List the authenticated account's hosted durable memory. */
447
- export function listMemories(options?: Options): Promise<readonly MemoryRecord[]>;
456
+ export function listMemories(options?: MemoryOptions): Promise<readonly MemoryRecord[]>;
448
457
  /** Compare-and-swap delete one hosted durable memory; deleting an absent id is idempotent. */
449
- export function deleteMemory(key: MemoryKey, options?: Options): Promise<void>;
450
- export function memory(operation: MemoryScanOperation, options?: Options): Promise<MemoryScanResult>;
451
- export function memory(operation: MemoryReadOperation, options?: Options): Promise<MemoryReadResult>;
452
- export function memory(operation: MemoryPutOperation, options?: Options): Promise<MemoryPutResult>;
453
- export function memory(operation: MemoryDeleteOperation, options?: Options): Promise<MemoryDeleteResult>;
454
- export function memory(operation: MemoryOperation, options?: Options): Promise<MemoryResult>;
458
+ export function deleteMemory(key: MemoryKey, options?: MemoryOptions): Promise<void>;
459
+ export function memory(operation: MemoryScanOperation, options?: MemoryOptions): Promise<MemoryScanResult>;
460
+ export function memory(operation: MemoryReadOperation, options?: MemoryOptions): Promise<MemoryReadResult>;
461
+ export function memory(operation: MemoryPutOperation, options?: MemoryOptions): Promise<MemoryPutResult>;
462
+ export function memory(operation: MemoryDeleteOperation, options?: MemoryOptions): Promise<MemoryDeleteResult>;
463
+ export function memory(operation: MemoryOperation, options?: MemoryOptions): Promise<MemoryResult>;
455
464
  export function getOrganization(options?: Options): Promise<Organization>;
456
465
  export function updateOrganization(request: OrganizationUpdate, options?: Options): Promise<Organization>;
457
466
 
package/managed/Agent.mjs CHANGED
@@ -1,5 +1,7 @@
1
+ import { requestOriginContext } from "../tools/environment.mjs";
1
2
  import { ManagedError } from "./ManagedError.mjs";
2
3
  import { registerManagedAgent } from "./internal.mjs";
4
+ import { managedAccessFetch } from "./Access.mjs";
3
5
 
4
6
  const API_KEY = /^ncx_live_[A-Za-z0-9_-]{12}_[A-Za-z0-9_-]{43}$/;
5
7
  const CURSOR = /^(?:0|[1-9][0-9]*)$/;
@@ -23,7 +25,7 @@ const TURN_SUBMISSION_TIMEOUT_MS = 10_000;
23
25
  const TURN_STATE_POLL_INITIAL_MS = 1_000;
24
26
  const TURN_STATE_POLL_MAX_MS = 5_000;
25
27
  const TURN_STATE_READ_TIMEOUT_MS = 2_000;
26
- const ALLOWED_OPTIONS = new Set(["apiKey", "baseUrl", "fetch", "toolsTransport"]);
28
+ const ALLOWED_OPTIONS = new Set(["apiKey", "baseUrl", "fetch", "toolsTransport", "requestOrigin"]);
27
29
  const CREATE_SETTINGS = new Set(["model", "thinking", "reasoningMode", "fastMode"]);
28
30
  const SETTINGS_PATCH = CREATE_SETTINGS;
29
31
  const MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra"]);
@@ -247,9 +249,16 @@ export async function readSession(request, options = {}) {
247
249
  return managedReadSessionResponse(body);
248
250
  }
249
251
 
252
+ function memoryClient(options) {
253
+ const { scope = "team", ...clientOptions } = options;
254
+ if (scope !== "team" && scope !== "personal") throw new TypeError("memory scope must be team or personal");
255
+ return { client: managedClient(clientOptions), scope };
256
+ }
257
+
250
258
  /** List the authenticated account's hosted durable memory. */
251
259
  export async function listMemories(options = {}) {
252
- const body = await managedClient(options).json("/v1/memory");
260
+ const { client, scope } = memoryClient(options);
261
+ const body = await client.json(`/v1/memory${scope === "personal" ? "?scope=personal" : ""}`);
253
262
  if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.memories)) {
254
263
  throw new ManagedError("invalid_response", "managed memory list is malformed");
255
264
  }
@@ -259,8 +268,9 @@ export async function listMemories(options = {}) {
259
268
  /** Compare-and-swap delete one account-owned hosted memory. */
260
269
  export async function deleteMemory(key, options = {}) {
261
270
  validateMemoryKey(key);
262
- await managedClient(options).empty(
263
- `/v1/memory/${key.id}?version=${key.version}`,
271
+ const { client, scope } = memoryClient(options);
272
+ await client.empty(
273
+ `/v1/memory/${key.id}?version=${key.version}${scope === "personal" ? "&scope=personal" : ""}`,
264
274
  { method: "DELETE" },
265
275
  );
266
276
  }
@@ -268,9 +278,10 @@ export async function deleteMemory(key, options = {}) {
268
278
  /** Run one atomic durable-memory operation in the authenticated account scope. */
269
279
  export async function memory(operation, options = {}) {
270
280
  validateMemoryOperation(operation);
271
- const body = await managedClient(options).json("/v1/memory", {
281
+ const { client, scope } = memoryClient(options);
282
+ const body = await client.json("/v1/memory", {
272
283
  method: "POST",
273
- body: JSON.stringify(operation),
284
+ body: JSON.stringify(scope === "personal" ? { ...operation, scope } : operation),
274
285
  });
275
286
  return managedMemoryResponse(body, operation.operation);
276
287
  }
@@ -302,6 +313,10 @@ function agentHandle(client, id, summary, retainedEventStream) {
302
313
  id,
303
314
  ...(summary === undefined ? {} : { summary }),
304
315
  events,
316
+ // Activation is explicit: passive event/history readers never warm a model.
317
+ prepare: (options = {}) => client.json(`${agentPath(id)}/prepare`, {
318
+ method: "POST", ...(options.signal === undefined ? {} : { signal: options.signal }),
319
+ }).then(() => undefined),
305
320
  requiredActions: Object.freeze({
306
321
  list: () => client.json(`${agentPath(id)}/required-actions`),
307
322
  submit: (callId, outcome) => {
@@ -1524,6 +1539,11 @@ function managedClient(options) {
1524
1539
  }
1525
1540
  const fetchImpl = options.fetch ?? globalThis.fetch;
1526
1541
  if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime");
1542
+ const authorizedFetch = managedAccessFetch(fetchImpl, new URL(baseUrl).origin, apiKey);
1543
+ const requestOrigin = requestOriginContext(options.requestOrigin ?? {
1544
+ client: typeof window === "undefined" ? "javascript" : "web",
1545
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1546
+ });
1527
1547
  const toolsTransport = options.toolsTransport;
1528
1548
  if (toolsTransport !== undefined
1529
1549
  && typeof toolsTransport !== "function"
@@ -1533,13 +1553,14 @@ function managedClient(options) {
1533
1553
 
1534
1554
  const response = async (path, init = {}) => {
1535
1555
  const headers = new Headers();
1556
+ if (path === "/v1/agents" || path.startsWith("/v1/agents/")) headers.set("x-nanocodex-client-context", JSON.stringify(requestOrigin));
1536
1557
  if (init.body !== undefined) headers.set("content-type", "application/json");
1537
1558
  if (init.accept) headers.set("accept", init.accept);
1538
1559
  if (init.idempotencyKey) headers.set("idempotency-key", init.idempotencyKey);
1539
1560
  if (init.voiceSessionId) headers.set("x-nanocodex-voice-session-id", init.voiceSessionId);
1540
1561
  if (apiKey) headers.set("authorization", `Bearer ${apiKey}`);
1541
1562
  try {
1542
- return await fetchImpl(new URL(path, baseUrl), {
1563
+ return await authorizedFetch(new URL(path, baseUrl), {
1543
1564
  method: init.method ?? "GET",
1544
1565
  headers,
1545
1566
  credentials: apiKey ? "omit" : "include",
package/managed/Voice.mjs CHANGED
@@ -50,6 +50,7 @@ export async function createManagedBrowserVoice(agent, voice, options = {}) {
50
50
  appendText: (role, text) => raw.appendText(role, text),
51
51
  appendContext: (text) => raw.appendContext(text),
52
52
  parallelStartup: true,
53
+ dataChannelControl: true,
53
54
  async start() {
54
55
  const started = await startManagedRealtime(agent, voiceSessionId, startOperationId);
55
56
  raw.start(JSON.stringify(started.context));
@@ -1,5 +1,6 @@
1
1
  export * as Agent from "./Agent.mjs";
2
2
  export { ManagedError } from "./ManagedError.mjs";
3
+ export { withManagedAccess } from "./Access.mjs";
3
4
  export type {
4
5
  Agent as ManagedAgent,
5
6
  CronTrigger as ManagedCronTrigger,
package/managed/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export * as Agent from "./Agent.mjs";
2
2
  export { ManagedError } from "./ManagedError.mjs";
3
+ export { withManagedAccess } from "./Access.mjs";
package/node/host.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { createResponsesHttp, responsesHttpHeaders } from "../runtime/responses-http.mjs";
1
2
  import { Console } from "node:console";
2
3
  import { createRequire } from "node:module";
3
4
  import { resolve } from "node:path";
@@ -41,6 +42,12 @@ export function createNodeHost(options = {}) {
41
42
  }
42
43
  const toolsLifecycle = options.tools?.[toolRuntimeLifecycle];
43
44
  toolsLifecycle?.available();
45
+ const http = createResponsesHttp((endpoint, apiKey, sessionId, metadata, body, signal) => {
46
+ if (disposal) throw new Error("Nanocodex host is already disposed");
47
+ if (options.mpp) throw JSON.stringify({ kind: "transport", detail: "MPP HTTPS transport is unavailable", reconnectable: false });
48
+ return fetch(endpoint, { method: "POST", headers: responsesHttpHeaders(apiKey, sessionId, metadata),
49
+ body, signal, redirect: "error" });
50
+ });
44
51
  const connections = new Map();
45
52
  const code = createCodeRuntime(options.tools, {
46
53
  require: createRequire(resolve(options.workspace ?? process.cwd(), ".nanocodex-code-mode.cjs")),
@@ -288,6 +295,7 @@ export function createNodeHost(options = {}) {
288
295
 
289
296
  function dispose() {
290
297
  if (disposal) return disposal;
298
+ http.dispose();
291
299
  disposal = Promise.resolve().then(() => settleCleanup([
292
300
  ...[...connections.keys()].map((handle) => () => close(handle)),
293
301
  () => code.reset(),
@@ -309,6 +317,10 @@ export function createNodeHost(options = {}) {
309
317
  if (references > 0) references -= 1;
310
318
  return references === 0 ? dispose() : Promise.resolve();
311
319
  },
320
+ httpOpen: http.httpOpen,
321
+ httpReady: http.httpReady,
322
+ httpNext: http.httpNext,
323
+ httpClose: http.httpClose,
312
324
  connect,
313
325
  send,
314
326
  next,