nanocodex 0.6.6 → 0.6.7

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 (114) hide show
  1. package/README.md +297 -6
  2. package/THIRD-PARTY-LICENSES +23 -0
  3. package/browser/Agent.d.mts +5 -4
  4. package/browser/Agent.mjs +4 -0
  5. package/browser/Claude.d.mts +4 -0
  6. package/browser/Claude.mjs +11 -0
  7. package/browser/InlineAgent.mjs +19 -1
  8. package/browser/host.d.mts +54 -1
  9. package/browser/host.mjs +65 -16
  10. package/browser/hostManagedWebSocket.mjs +11 -2
  11. package/browser/index.d.mts +3 -10
  12. package/browser/index.mjs +2 -5
  13. package/browser/socket-observations.mjs +203 -0
  14. package/cloud/Decorator.d.mts +2 -2
  15. package/cloud/Decorator.mjs +1 -0
  16. package/cloud/Dialog.d.mts +13 -0
  17. package/cloud/Dialog.mjs +52 -6
  18. package/cloud/Transport.d.mts +0 -11
  19. package/cloud/Transport.mjs +1 -386
  20. package/cloud/actions/account.d.mts +12 -0
  21. package/cloud/actions/account.mjs +11 -0
  22. package/cloud/actions/agent.mjs +3 -0
  23. package/cloud/actions/connection.d.mts +3 -1
  24. package/cloud/actions/connection.mjs +19 -4
  25. package/cloud/index.d.mts +1 -1
  26. package/cloud/index.mjs +1 -1
  27. package/cloud/internal.mjs +19 -4
  28. package/cloud/mercator.mjs +1 -1
  29. package/cloud/mock.d.mts +17 -0
  30. package/cloud/mock.mjs +392 -0
  31. package/cloud/types.d.mts +3 -1
  32. package/cloudflare/Agent.d.mts +2 -0
  33. package/cloudflare/Agent.mjs +88 -2
  34. package/cloudflare/agent-settings.d.mts +1 -1
  35. package/cloudflare/agent-settings.mjs +9 -0
  36. package/cloudflare/egress.mjs +9 -6
  37. package/cloudflare/gateway-responses.d.mts +1 -0
  38. package/cloudflare/gateway-responses.mjs +2 -2
  39. package/cloudflare/managed-auth.d.mts +4 -4
  40. package/cloudflare/managed-auth.mjs +14 -2
  41. package/cloudflare/managed-live.d.mts +6 -0
  42. package/cloudflare/managed-live.mjs +56 -1
  43. package/cloudflare/workers-ai-responses.d.mts +2 -1
  44. package/cloudflare/workers-ai-responses.mjs +80 -6
  45. package/host/Agent.d.mts +6 -2
  46. package/host/Claude.d.mts +3 -0
  47. package/host/Claude.mjs +2 -0
  48. package/host/index.d.mts +6 -10
  49. package/host/index.mjs +2 -5
  50. package/index.d.mts +11 -10
  51. package/index.mjs +0 -5
  52. package/internal.mjs +28 -3
  53. package/managed/Agent.d.mts +6 -2
  54. package/managed/Agent.mjs +39 -5
  55. package/managed/ManagedError.d.mts +3 -0
  56. package/node/Agent.d.mts +6 -2
  57. package/node/Agent.mjs +14 -1
  58. package/node/Claude.d.mts +4 -0
  59. package/node/Claude.mjs +24 -0
  60. package/node/host.mjs +18 -9
  61. package/node/index.d.mts +6 -10
  62. package/node/index.mjs +2 -5
  63. package/package.json +41 -9
  64. package/pkg-node/nanocodex.d.ts +109 -0
  65. package/pkg-node/nanocodex.js +818 -51
  66. package/pkg-node/nanocodex_bg.wasm.d.ts +47 -3
  67. package/pkg-web/.nanocodex-bindgen-stamp +3 -3
  68. package/pkg-web/nanocodex-build.json +1 -1
  69. package/pkg-web/nanocodex.d.ts +156 -3
  70. package/pkg-web/nanocodex.js +813 -51
  71. package/pkg-web/nanocodex_bg.js +813 -51
  72. package/pkg-web/nanocodex_bg.wasm +0 -0
  73. package/pkg-web/nanocodex_bg.wasm.d.ts +47 -3
  74. package/pkg-web/nanocodex_worker.js +1 -1
  75. package/runtime/claude-host.mjs +308 -0
  76. package/runtime/claude-subscription.mjs +79 -0
  77. package/runtime/claude.d.mts +112 -0
  78. package/runtime/claude.mjs +221 -0
  79. package/runtime/code-effect-identity.mjs +123 -0
  80. package/runtime/harnesses.mjs +51 -0
  81. package/runtime/mcp-payment.mjs +8 -0
  82. package/runtime/mcp-runtime.d.mts +10 -0
  83. package/runtime/mcp-runtime.mjs +242 -57
  84. package/runtime/mercator.mjs +2 -0
  85. package/runtime/quickjs-evaluator.mjs +23 -11
  86. package/runtime/response-controls.mjs +1 -6
  87. package/runtime/responses-http.mjs +4 -2
  88. package/runtime/retry-after.d.mts +10 -0
  89. package/runtime/retry-after.mjs +86 -0
  90. package/runtime/steer-receipt.mjs +8 -1
  91. package/runtime/subagent-routing.d.mts +4 -2
  92. package/runtime/subagent-routing.mjs +7 -2
  93. package/runtime/subagents.d.mts +8 -3
  94. package/services/account.d.mts +76 -0
  95. package/services/account.mjs +151 -0
  96. package/services/hosted.mjs +72 -0
  97. package/services/index.d.mts +95 -0
  98. package/services/index.mjs +192 -0
  99. package/services/scope.mjs +36 -0
  100. package/{runtime/tempo-provider.d.mts → tempo/index.d.mts} +20 -1
  101. package/{runtime/tempo-provider.mjs → tempo/index.mjs} +39 -3
  102. package/tools/browser/browserShell.mjs +18 -8
  103. package/tools/browser/python.worker.mjs +17 -1
  104. package/tools/browser/threadGit.mjs +12 -2
  105. package/tools/environment.d.mts +8 -0
  106. package/tools/environment.mjs +10 -0
  107. package/tools/index.d.mts +6 -3
  108. package/types.d.mts +104 -4
  109. package/worker/Claude.d.mts +4 -0
  110. package/worker/Claude.mjs +11 -0
  111. package/worker/ClaudeSubscription.d.mts +24 -0
  112. package/worker/ClaudeSubscription.mjs +9 -0
  113. package/worker/index.d.mts +9 -0
  114. package/worker/index.mjs +4 -0
@@ -1,3 +1,4 @@
1
+ import { normalizeServices, serviceResource } from '../../services/scope.mjs';
1
2
  import {
2
3
  connectionRequestFromGrant,
3
4
  connectionFromWire,
@@ -57,11 +58,14 @@ const AGENT_VISIBILITY_NAMES = Object.freeze({
57
58
 
58
59
  export async function connect(client, options) {
59
60
  options ??= {};
60
- const permission = options.permission ?? "agent.run";
61
+ const services = normalizeServices(options.capabilities?.services);
62
+ const permission = options.permission ?? (services ? "services.use" : "agent.run");
63
+ const standalone = permission === "services.use";
64
+ if (standalone && !services) throw new TypeError("services.use requires exact service capabilities");
61
65
  if (typeof permission !== "string" || permission.length === 0) throw new TypeError("connect permission must be a non-empty string");
62
66
  const requestedConnectors = normalizeCloudAccounts(options.capabilities?.cloudAccounts);
63
- const agentVisibility = normalizeAgentVisibility(options.capabilities?.agent);
64
- const authorization = options.authorization ?? (client.principal ? "hosted" : "access_key");
67
+ const agentVisibility = normalizeAgentVisibility(options.capabilities?.agent ?? (standalone ? { finalMessages: false, actionSummaries: false } : undefined));
68
+ const authorization = options.authorization ?? (client.principal || standalone ? "hosted" : "access_key");
65
69
  if (authorization !== "access_key" && authorization !== "hosted") {
66
70
  throw new TypeError("connect authorization must be access_key or hosted");
67
71
  }
@@ -71,6 +75,9 @@ export async function connect(client, options) {
71
75
  if (client.principal && options.capabilities?.authorizeAccessKey !== undefined) {
72
76
  throw new TypeError("host principal connections cannot request access-key or MPP authority");
73
77
  }
78
+ if (standalone && (authorization !== "hosted" || options.capabilities?.authorizeAccessKey || options.capabilities?.agent || requestedConnectors.includes("chatgpt") || options.tools?.length || options.mcpConnections?.length || options.conversationId)) {
79
+ throw new TypeError("Standalone services require hosted authorization without agent or payment capabilities");
80
+ }
74
81
  const appToolCatalog = hostedAppToolCatalog(options.tools ?? []);
75
82
  const appToolCatalogDigest = appToolCatalog.length === 0
76
83
  ? undefined
@@ -79,6 +86,7 @@ export async function connect(client, options) {
79
86
  const mcpConnections = normalizeMcpConnections(options.mcpConnections ?? []);
80
87
  const focusMcpConnectionId = normalizeMcpFocus(options.focusMcpConnectionId, mcpConnections);
81
88
  const exactRequest = connectionRequestFromGrant({
89
+ services,
82
90
  connectors: requestedConnectors,
83
91
  mcpConnections,
84
92
  permission,
@@ -98,6 +106,8 @@ export async function connect(client, options) {
98
106
  conversationId,
99
107
  authorization,
100
108
  appToolCatalogDigest,
109
+ services,
110
+ standalone,
101
111
  );
102
112
  if (client.principal
103
113
  && baseAuth.resources.some((resource) => resource.startsWith(MPP_RESOURCE_PREFIX))) {
@@ -311,10 +321,14 @@ function withConnectionResources(
311
321
  conversationId,
312
322
  authorization,
313
323
  appToolCatalogDigest,
324
+ services,
325
+ standalone,
314
326
  ) {
315
327
  const configured = typeof auth === "object" && auth !== null
316
328
  ? (auth.resources ?? []).filter((resource) =>
317
- !Object.values(AGENT_VISIBILITY_RESOURCES).includes(resource)
329
+ !(standalone && (resource.startsWith("urn:nanocodex:agent:") || resource.startsWith(MPP_RESOURCE_PREFIX)))
330
+ && !resource.startsWith("urn:nanocodex:services:")
331
+ && !Object.values(AGENT_VISIBILITY_RESOURCES).includes(resource)
318
332
  && !resource.startsWith(AGENT_VISIBILITY_RESOURCE_PREFIX)
319
333
  && !resource.startsWith(AGENT_CONVERSATION_RESOURCE_PREFIX)
320
334
  && !resource.startsWith(CONNECTOR_RESOURCE_PREFIX)
@@ -332,6 +346,7 @@ function withConnectionResources(
332
346
  .map(([, value]) => value);
333
347
  const resources = [...new Set([
334
348
  ...configured,
349
+ ...(services ? [serviceResource(services)] : []),
335
350
  `${APP_RESOURCE_PREFIX}${encodeURIComponent(appId)}`,
336
351
  ...(appOrigin ? [`${APP_ORIGIN_RESOURCE_PREFIX}${encodeURIComponent(appOrigin)}`] : []),
337
352
  ...(requestedConnectors.length === 0
package/cloud/index.d.mts CHANGED
@@ -6,7 +6,7 @@ export * as Principal from "./Principal.mjs";
6
6
  export * as Transport from "./Transport.mjs";
7
7
  export { connectActions } from "./Decorator.mjs";
8
8
  export { iframe, popup } from "./Dialog.mjs";
9
- export { http, mock } from "./Transport.mjs";
9
+ export { http } from "./Transport.mjs";
10
10
  export type {
11
11
  AccessKey,
12
12
  AgentTurn,
package/cloud/index.mjs CHANGED
@@ -6,4 +6,4 @@ export * as Principal from "./Principal.mjs";
6
6
  export * as Transport from "./Transport.mjs";
7
7
  export { connectActions } from "./Decorator.mjs";
8
8
  export { iframe, popup } from "./Dialog.mjs";
9
- export { http, mock } from "./Transport.mjs";
9
+ export { http } from "./Transport.mjs";
@@ -1,3 +1,4 @@
1
+ import { normalizeServices } from '../services/scope.mjs';
1
2
  import { InvalidResponseError } from "./Errors.mjs";
2
3
 
3
4
  const CLOUD_ACCOUNT_PROVIDERS = Object.freeze([
@@ -50,6 +51,11 @@ export function connectionFromWire(value) {
50
51
  throw new InvalidResponseError("access-key connections require access-key and MPP authority");
51
52
  }
52
53
  const capabilities = strings(grant.capabilities, "connection.grant.capabilities");
54
+ if (grant.permission === "services.use" && (wire.agent_id !== undefined || authorization !== "hosted"
55
+ || grant.services === undefined || grant.conversation_id !== undefined || grant.app_tool_catalog_digest !== undefined
56
+ || capabilities.some(value => value === "nanocodex.agent" || value === "chatgpt" || value.startsWith("agent.") || value.startsWith("mcp:")))) {
57
+ throw new InvalidResponseError("Standalone service grants cannot contain agent or payment authority");
58
+ }
53
59
  const grantMcpConnections = mcpConnections(
54
60
  grant.mcp_connections,
55
61
  "connection.grant.mcp_connections",
@@ -67,7 +73,7 @@ export function connectionFromWire(value) {
67
73
  );
68
74
  return Object.freeze({
69
75
  ...owner,
70
- agentId: string(wire.agent_id, "connection.agent_id"),
76
+ ...(grant.permission === "services.use" && wire.agent_id === undefined ? {} : { agentId: string(wire.agent_id, "connection.agent_id") }),
71
77
  grant: Object.freeze({
72
78
  id: hex(grant.id, "connection.grant.id"),
73
79
  permission: string(grant.permission, "connection.grant.permission"),
@@ -76,13 +82,14 @@ export function connectionFromWire(value) {
76
82
  ...(grant.conversation_id === undefined ? {} : {
77
83
  conversationId: agentConversationId(grant.conversation_id, "connection.grant.conversation_id"),
78
84
  }),
85
+ ...(grant.services === undefined ? {} : { services: normalizeServices(grant.services) }),
79
86
  capabilities,
80
87
  connectors: grantConnectors,
81
88
  ...(grantConnectorConnections === undefined ? {} : {
82
89
  connectorConnections: grantConnectorConnections,
83
90
  }),
84
91
  mcpConnections: grantMcpConnections,
85
- visibility: agentVisibility(capabilities),
92
+ visibility: agentVisibility(capabilities, grant.permission === "services.use"),
86
93
  ...(grant.app_tool_catalog_digest === undefined ? {} : {
87
94
  appToolCatalogDigest: catalogDigest(
88
95
  grant.app_tool_catalog_digest,
@@ -135,6 +142,11 @@ export function connectionMatchesRequest(connection, options = {}) {
135
142
  return false;
136
143
  }
137
144
  }
145
+ if (Object.hasOwn(options.capabilities ?? {}, "services")) {
146
+ try {
147
+ if (JSON.stringify(normalizeServices(options.capabilities.services)) !== JSON.stringify(normalizeServices(connection.grant.services))) return false;
148
+ } catch { return false; }
149
+ }
138
150
  const requestedCloudAccounts = options.capabilities?.cloudAccounts;
139
151
  if (requestedCloudAccounts !== undefined) {
140
152
  const requested = CLOUD_ACCOUNT_PROVIDERS.filter(
@@ -190,6 +202,7 @@ export function reconnectRequestFromConnection(connection) {
190
202
  export function connectionRequestFromGrant(grant) {
191
203
  return Object.freeze({
192
204
  capabilities: Object.freeze({
205
+ services: grant.services,
193
206
  agent: grant.visibility,
194
207
  cloudAccounts: Object.freeze(Object.fromEntries(
195
208
  grant.connectors.map((provider) => [provider, true]),
@@ -297,13 +310,14 @@ export function grantFromWire(value) {
297
310
  ...(grant.conversation_id === undefined ? {} : {
298
311
  conversationId: agentConversationId(grant.conversation_id, "grant.conversation_id"),
299
312
  }),
313
+ ...(grant.services === undefined ? {} : { services: normalizeServices(grant.services) }),
300
314
  capabilities,
301
315
  connectors: grantConnectors,
302
316
  ...(grantConnectorConnections === undefined ? {} : {
303
317
  connectorConnections: grantConnectorConnections,
304
318
  }),
305
319
  mcpConnections: grantMcpConnections,
306
- visibility: agentVisibility(capabilities),
320
+ visibility: agentVisibility(capabilities, grant.permission === "services.use"),
307
321
  ...(grant.app_tool_catalog_digest === undefined ? {} : {
308
322
  appToolCatalogDigest: catalogDigest(grant.app_tool_catalog_digest, "grant.app_tool_catalog_digest"),
309
323
  }),
@@ -329,7 +343,8 @@ const AGENT_VISIBILITY_CAPABILITIES = Object.freeze({
329
343
  rawTraces: "agent.trace.read",
330
344
  });
331
345
 
332
- function agentVisibility(capabilities) {
346
+ function agentVisibility(capabilities, standalone = false) {
347
+ if (standalone) return Object.freeze({ finalMessages: false, actionSummaries: false, conversationHistory: false, rawTraces: false });
333
348
  const recognized = Object.values(AGENT_VISIBILITY_CAPABILITIES)
334
349
  .some((capability) => capabilities.includes(capability));
335
350
  const rawTraces = capabilities.includes(AGENT_VISIBILITY_CAPABILITIES.rawTraces);
@@ -1,4 +1,4 @@
1
- import { DEFAULT_MERCATOR_MCP_URL } from "../runtime/tempo-provider.mjs";
1
+ import { DEFAULT_MERCATOR_MCP_URL } from "../runtime/mercator.mjs";
2
2
 
3
3
  const MERCATOR_ORIGIN = new URL(DEFAULT_MERCATOR_MCP_URL).origin;
4
4
  const JOBS_PATH = "/v1/jobs";
@@ -0,0 +1,17 @@
1
+ import type { Transport } from "./Transport.mjs";
2
+
3
+ /**
4
+ * In-memory Nanocodex Connect API for tests and demos. Requires the optional
5
+ * `ox` peer dependency, which computes the EIP-191 access-key witness.
6
+ */
7
+ export const MOCK_ACCOUNT_ADDRESS: `0x${string}`;
8
+ export const MOCK_MACHINE_USD_ADDRESS: `0x${string}`;
9
+
10
+ export function mock(options?: Readonly<{
11
+ accountAddress?: `0x${string}` | undefined;
12
+ appName?: string | undefined;
13
+ appOrigin?: string | undefined;
14
+ key?: string | undefined;
15
+ machineUsdAddress?: `0x${string}` | undefined;
16
+ name?: string | undefined;
17
+ }>): Transport<"mock">;
package/cloud/mock.mjs ADDED
@@ -0,0 +1,392 @@
1
+ // In-memory Nanocodex Connect API for tests and demos. Its prepared access-key
2
+ // witness is the real EIP-191 digest, so this module needs the optional `ox`
3
+ // peer dependency; the core `nanocodex/connect` entry point never imports it.
4
+ import { Hex, PersonalMessage } from "ox";
5
+
6
+ import { HttpError, InvalidResponseError } from "./Errors.mjs";
7
+ import { from } from "./Transport.mjs";
8
+
9
+ export const MOCK_ACCOUNT_ADDRESS = "0x8ba1f109551bd432803012645ac136ddd64dba72";
10
+ export const MOCK_MACHINE_USD_ADDRESS = "0x20c0000000000000000000000000000000000001";
11
+
12
+ export function mock(options = {}) {
13
+ const accountAddress = options.accountAddress ?? MOCK_ACCOUNT_ADDRESS;
14
+ const machineUsdAddress = options.machineUsdAddress ?? MOCK_MACHINE_USD_ADDRESS;
15
+ const appName = options.appName ?? "Acme Workspace";
16
+ const grants = new Map();
17
+ let sequence = 0;
18
+
19
+ return from({
20
+ key: options.key ?? "mock",
21
+ name: options.name ?? "Nanocodex mock",
22
+ type: "mock",
23
+ setup({ appId }) {
24
+ return {
25
+ baseUrl: "memory://nanocodex",
26
+ async request(request) {
27
+ if (request.method === "POST" && request.path === "/v1/connections") {
28
+ const authorization = request.body?.key_authorization;
29
+ const keyId = authorization?.keyId ?? authorization?.address;
30
+ if (typeof keyId !== "string") throw new InvalidResponseError("key authorization is missing keyId");
31
+ const expiry = authorization.expiry ?? Math.floor(Date.now() / 1000) + 30 * 86_400;
32
+ const connectors = requestedConnectors(request.body?.requested_connectors);
33
+ const mcpConnections = requestedMcpConnections(request.body?.requested_mcp_connections);
34
+ const grantId = mockHex(`${appId}:${accountAddress}:${keyId}`, 32);
35
+ const connectorConnections = mockConnectorConnections(connectors, grantId);
36
+ const wire = {
37
+ grant_token: `mock-grant-${grantId.slice(2)}`,
38
+ account_address: accountAddress,
39
+ agent_id: `agent_${grantId.slice(2, 14)}`,
40
+ grant: {
41
+ id: grantId,
42
+ permission: request.body?.permission ?? "agent.run",
43
+ status: "active",
44
+ expires_at: expiry,
45
+ capabilities: [
46
+ "nanocodex.agent",
47
+ "mercator.boost",
48
+ "mpp.mach",
49
+ ...connectors,
50
+ ...mcpConnections.map(({ id }) => `mcp:${id}`),
51
+ ],
52
+ connector_connections: connectorConnections,
53
+ mcp_connections: mcpConnections,
54
+ },
55
+ access_key: {
56
+ address: keyId,
57
+ chain_id: String(authorization.chainId ?? 4217),
58
+ key_id: keyId,
59
+ key_type: authorization.keyType ?? "p256",
60
+ limits: authorization.limits ?? [{ token: machineUsdAddress, limit: "10000000", period: 86_400 }],
61
+ scopes: authorization.scopes ?? [],
62
+ witness: authorization.witness ?? mockHex(`${grantId}:witness`, 32),
63
+ expiry,
64
+ authorization: request.body?.signed_key_authorization ?? "0x01",
65
+ },
66
+ mpp: {
67
+ token: machineUsdAddress,
68
+ symbol: "MACH",
69
+ balance_status: "ready",
70
+ settlement_token: "0x20C000000000000000000000b9537d11c60E8b50",
71
+ settlement_symbol: "USDC.e",
72
+ settlement_balance_atomics: "0",
73
+ limit_atomics: "10000000",
74
+ max_per_request_atomics: "250000",
75
+ period: 86_400,
76
+ balance_atomics: "0",
77
+ spent_atomics: "0",
78
+ },
79
+ };
80
+ grants.set(grantId, wire);
81
+ return wire;
82
+ }
83
+
84
+ if (request.method === "GET" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+$/)) {
85
+ return requiredGrant(grants, request.path.split("/")[3]);
86
+ }
87
+
88
+ if (request.method === "POST" && request.path === "/v1/connections/prepare") {
89
+ const permission = request.body?.permission;
90
+ if (typeof permission !== "string" || permission.length === 0) {
91
+ throw new TypeError("connection permission must be a non-empty string");
92
+ }
93
+ const resources = request.body?.resources ?? [];
94
+ if (!Array.isArray(resources) || resources.some((resource) => typeof resource !== "string" || resource.length === 0)) {
95
+ throw new TypeError("connection resources must be non-empty strings");
96
+ }
97
+ const connectors = connectorResources(resources);
98
+ const id = `req_${++sequence}`;
99
+ const accessPolicy = request.body?.authorize_access_key;
100
+ const keyId = accessPolicy?.address ?? mockHex(`${appId}:${accountAddress}:${permission}:key`, 20);
101
+ const tokenLimit = accessPolicy?.limits?.find((limit) => limit.token === machineUsdAddress);
102
+ const expiry = accessPolicy?.expiry ?? Math.floor(Date.now() / 1000) + 30 * 86_400;
103
+ const dailyLimit = tokenLimit?.limit ?? "10000000";
104
+ const period = tokenLimit?.period ?? 86_400;
105
+ const appOrigin = options.appOrigin ?? "https://app.example";
106
+ const appUrl = new URL(appOrigin);
107
+ const nonce = mockHex(`${id}:siwe-nonce`, 8).slice(2);
108
+ const issuedAt = new Date().toISOString();
109
+ const authMessage = [
110
+ `${appUrl.host} wants you to sign in with your Ethereum account:`,
111
+ "0x0000000000000000000000000000000000000000",
112
+ "",
113
+ `${appName} wants to connect your Nanocodex agent.`,
114
+ "",
115
+ `URI: ${appUrl.origin}`,
116
+ "Version: 1",
117
+ "Chain ID: 4217",
118
+ `Nonce: ${nonce}`,
119
+ `Issued At: ${issuedAt}`,
120
+ ...(resources.length > 0
121
+ ? ["Resources:", ...resources.map((resource) => `- ${resource}`)]
122
+ : []),
123
+ ].join("\n");
124
+ return {
125
+ request_id: id,
126
+ app: { id: appId, name: appName, origin: appOrigin },
127
+ account_address: accountAddress,
128
+ auth: {
129
+ message: authMessage,
130
+ resources,
131
+ },
132
+ permission: {
133
+ id: permission,
134
+ title: "Use your Nanocodex agent",
135
+ description: "Run an app-owned Nanocodex agent with your approved capabilities.",
136
+ connectors: [
137
+ { id: "agent", name: "Nanocodex", detail: "Instantiate the real Nanocodex runtime in this app" },
138
+ { id: "mercator", name: "BOOST with Mercator", detail: "Find and compose the right tools, paying per call through MPP" },
139
+ ...connectors.map((provider) => ({
140
+ id: provider,
141
+ name: connectorName(provider),
142
+ detail: `Use the connected ${connectorName(provider)} account through the grant`,
143
+ })),
144
+ ],
145
+ },
146
+ access_key: {
147
+ address: keyId,
148
+ chain_id: accessPolicy?.chainId ?? "4217",
149
+ key_id: keyId,
150
+ public_key: accessPolicy?.publicKey ?? mockHex(`${id}:public`, 65),
151
+ key_type: accessPolicy?.keyType ?? "p256",
152
+ limits: accessPolicy?.limits ?? [{ token: machineUsdAddress, limit: dailyLimit, period }],
153
+ scopes: accessPolicy?.scopes ?? [],
154
+ witness: PersonalMessage.getSignPayload(Hex.fromString(authMessage)),
155
+ expiry,
156
+ },
157
+ mpp: {
158
+ token: machineUsdAddress,
159
+ symbol: "MACH",
160
+ settlement_token: "0x20C000000000000000000000b9537d11c60E8b50",
161
+ settlement_symbol: "USDC.e",
162
+ settlement_balance_atomics: "0",
163
+ limit_atomics: dailyLimit,
164
+ period,
165
+ max_per_request_atomics: "250000",
166
+ },
167
+ };
168
+ }
169
+
170
+ if (request.method === "POST" && request.path === "/v1/connections/complete") {
171
+ const prepared = request.body?.prepared;
172
+ const approval = request.body?.approval;
173
+ if (!prepared || !approval || approval.approved !== true) {
174
+ throw new InvalidResponseError("connection approval is malformed");
175
+ }
176
+ const approvedCapabilities = approval.capabilities;
177
+ if (
178
+ approvedCapabilities?.keyAuthorization?.witness !== prepared.access_key.witness
179
+ || approvedCapabilities?.personalSign?.message !== prepared.auth.message
180
+ || typeof approvedCapabilities?.personalSign?.keyAuthorization !== "string"
181
+ || typeof approvedCapabilities?.signature !== "string"
182
+ ) {
183
+ throw new InvalidResponseError("connection approval is not bound to the SIWE challenge and key authorization");
184
+ }
185
+ const grantId = mockHex(`${prepared.app.id}:${prepared.account_address}:${prepared.access_key.key_id}`, 32);
186
+ const wire = {
187
+ grant_token: `mock-grant-${grantId.slice(2)}`,
188
+ account_address: prepared.account_address,
189
+ agent_id: `agent_${grantId.slice(2, 14)}`,
190
+ grant: {
191
+ id: grantId,
192
+ permission: prepared.permission.id,
193
+ status: "active",
194
+ expires_at: prepared.access_key.expiry,
195
+ capabilities: prepared.permission.connectors.map(({ id }) => id),
196
+ },
197
+ access_key: prepared.access_key,
198
+ mpp: {
199
+ ...prepared.mpp,
200
+ balance_atomics: "0",
201
+ balance_status: "ready",
202
+ spent_atomics: "0",
203
+ },
204
+ };
205
+ grants.set(grantId, wire);
206
+ return wire;
207
+ }
208
+
209
+ if (request.method === "POST" && request.path === "/v1/connections/disconnect") {
210
+ return undefined;
211
+ }
212
+
213
+ if (request.method === "GET" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/mpp\/balance$/)) {
214
+ const wire = requiredGrant(grants, request.path.split("/")[3]);
215
+ wire.mpp = { ...wire.mpp, balance_status: "ready" };
216
+ return wire;
217
+ }
218
+
219
+ if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/revoke$/)) {
220
+ const grantId = request.path.split("/")[3];
221
+ const wire = requiredGrant(grants, grantId);
222
+ wire.grant = { ...wire.grant, status: "revoked" };
223
+ return wire.grant;
224
+ }
225
+
226
+ if (request.method === "GET" && request.path === "/v1/machine-usd/config") {
227
+ return {
228
+ chain_id: 4217,
229
+ min_usd_amount_cents: 500,
230
+ max_usd_amount_cents: 10_000,
231
+ stripe_publishable_key: "pk_test_nanocodex",
232
+ token_address: machineUsdAddress,
233
+ };
234
+ }
235
+
236
+ if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/fund$/)) {
237
+ const grantId = request.path.split("/")[3];
238
+ const wire = requiredActiveGrant(grants, grantId);
239
+ const usdAmountCents = request.body?.usd_amount_cents;
240
+ if (!Number.isSafeInteger(usdAmountCents) || usdAmountCents < 500 || usdAmountCents > 10_000) {
241
+ throw new TypeError("MACH amount must be from 500 through 10000 cents");
242
+ }
243
+ const amount = BigInt(usdAmountCents) * 10_000n;
244
+ wire.mpp = {
245
+ ...wire.mpp,
246
+ balance_atomics: String(BigInt(wire.mpp.balance_atomics) + amount),
247
+ };
248
+ return {
249
+ order: {
250
+ id: `ord_${++sequence}`,
251
+ status: "complete",
252
+ usd_amount_cents: usdAmountCents,
253
+ machine_usd_amount_atomics: String(amount),
254
+ issuance_transaction_hash: mockHex(`fund:${sequence}`, 32),
255
+ },
256
+ connection: wire,
257
+ };
258
+ }
259
+
260
+ if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/mpp\/charge$/)) {
261
+ const grantId = request.path.split("/")[3];
262
+ const wire = requiredActiveGrant(grants, grantId);
263
+ const amount = BigInt(request.body?.amount_atomics ?? "-1");
264
+ const max = BigInt(wire.mpp.max_per_request_atomics);
265
+ const spent = BigInt(wire.mpp.spent_atomics);
266
+ const limit = BigInt(wire.mpp.limit_atomics);
267
+ const balance = BigInt(wire.mpp.balance_atomics);
268
+ if (amount <= 0n) throw new TypeError("MPP amount must be positive");
269
+ if (amount > max) throw new HttpError(403, "This payment exceeds the per-request permission.", { code: "mpp_request_limit_exceeded" });
270
+ if (spent + amount > limit) throw new HttpError(403, "This payment exceeds the daily MPP permission.", { code: "mpp_period_limit_exceeded" });
271
+ if (amount > balance) throw new HttpError(402, "Add MACH before paying for this capability.", { code: "machine_usd_required" });
272
+ wire.mpp = {
273
+ ...wire.mpp,
274
+ balance_atomics: String(balance - amount),
275
+ spent_atomics: String(spent + amount),
276
+ };
277
+ return {
278
+ receipt: {
279
+ id: `mpp_${++sequence}`,
280
+ amount_atomics: String(amount),
281
+ origin: request.body?.origin,
282
+ transaction_hash: mockHex(`mpp:${sequence}`, 32),
283
+ },
284
+ connection: wire,
285
+ };
286
+ }
287
+
288
+ throw new HttpError(404, `No mock route for ${request.method ?? "GET"} ${request.path}`, {
289
+ code: "mock_route_not_found",
290
+ });
291
+ },
292
+ };
293
+ },
294
+ });
295
+ }
296
+
297
+ function requiredGrant(grants, grantId) {
298
+ const grant = grants.get(grantId);
299
+ if (!grant) throw new HttpError(404, "Nanocodex grant not found", { code: "grant_not_found" });
300
+ return grant;
301
+ }
302
+
303
+ function requestedConnectors(value) {
304
+ if (value === undefined) return [];
305
+ if (!Array.isArray(value)) throw new TypeError("requested_connectors must be an array");
306
+ const supported = [
307
+ "github",
308
+ "gmail",
309
+ "gdrive",
310
+ "gcalendar",
311
+ "gtasks",
312
+ "gdocs",
313
+ "gsheets",
314
+ "gslides",
315
+ "gcontacts",
316
+ "slack",
317
+ "x",
318
+ "spotify",
319
+ "soundcloud",
320
+ "link",
321
+ "chatgpt",
322
+ ];
323
+ if (value.some((provider) => !supported.includes(provider))) {
324
+ throw new TypeError("requested_connectors contains an unsupported provider");
325
+ }
326
+ return supported.filter((provider) => value.includes(provider));
327
+ }
328
+
329
+ function requestedMcpConnections(value) {
330
+ if (value === undefined) return [];
331
+ if (!Array.isArray(value) || value.length > 16
332
+ || value.some((id) => typeof id !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(id))
333
+ || new Set(value).size !== value.length) {
334
+ throw new TypeError("requested_mcp_connections must contain exact hosted connection IDs");
335
+ }
336
+ return value.map((id) => ({ id, name: "MCP connection" }));
337
+ }
338
+
339
+ function connectorResources(resources) {
340
+ return requestedConnectors(resources
341
+ .filter((resource) => resource.startsWith("urn:nanocodex:connector:"))
342
+ .map((resource) => resource.slice("urn:nanocodex:connector:".length)));
343
+ }
344
+
345
+ function connectorName(provider) {
346
+ return ({
347
+ github: "GitHub",
348
+ gmail: "Gmail",
349
+ gdrive: "Google Drive",
350
+ gcalendar: "Google Calendar",
351
+ gtasks: "Google Tasks",
352
+ gdocs: "Google Docs",
353
+ gsheets: "Google Sheets",
354
+ gslides: "Google Slides",
355
+ gcontacts: "Google Contacts",
356
+ slack: "Slack",
357
+ x: "X",
358
+ spotify: "Spotify",
359
+ soundcloud: "SoundCloud",
360
+ link: "Stripe Link",
361
+ chatgpt: "ChatGPT",
362
+ })[provider];
363
+ }
364
+
365
+ function mockConnectorConnections(connectors, grantId) {
366
+ return Object.fromEntries(connectors
367
+ .filter((connector) => connector !== "chatgpt")
368
+ .map((connector) => [
369
+ connector,
370
+ [mockHex(`${grantId}:${connector}:connection`, 32).slice(2, 45)],
371
+ ]));
372
+ }
373
+
374
+ function requiredActiveGrant(grants, grantId) {
375
+ const grant = requiredGrant(grants, grantId);
376
+ if (grant.grant.status !== "active") {
377
+ throw new HttpError(403, "This Nanocodex grant has been revoked.", { code: "grant_revoked" });
378
+ }
379
+ return grant;
380
+ }
381
+
382
+ function mockHex(seed, bytes) {
383
+ let state = 2166136261;
384
+ let output = "";
385
+ for (let index = 0; output.length < bytes * 2; index += 1) {
386
+ const code = seed.charCodeAt(index % seed.length) + index;
387
+ state ^= code;
388
+ state = Math.imul(state, 16777619) >>> 0;
389
+ output += state.toString(16).padStart(8, "0");
390
+ }
391
+ return `0x${output.slice(0, bytes * 2)}`;
392
+ }
package/cloud/types.d.mts CHANGED
@@ -120,6 +120,7 @@ export type ConnectAgent = Readonly<{
120
120
  }>;
121
121
 
122
122
  export type Grant = Readonly<{
123
+ services?: import("../services/index.mjs").ServiceCapabilities | undefined;
123
124
  id: Hex;
124
125
  permission: string;
125
126
  status: "active" | "revoked" | "expired";
@@ -153,7 +154,8 @@ export type MppPermission = Readonly<{
153
154
  }>;
154
155
 
155
156
  type ConnectionBase = Readonly<{
156
- agentId: string;
157
+ /** Absent on a standalone services grant. */
158
+ agentId?: string;
157
159
  grant: Grant;
158
160
  }>;
159
161
 
@@ -96,6 +96,8 @@ export function create(owner: create.Owner, options?: create.Options): Promise<c
96
96
  export declare namespace create {
97
97
  type Owner = DurableObjectOwner;
98
98
  type Options = Readonly<{
99
+ /** Observer-only instant steering; disabled unless explicitly enabled. */
100
+ instantToolSteering?: boolean | undefined;
99
101
  /** Awaited host preservation barrier; scoped to this root, never inherited by children. */
100
102
  beforeCompaction?: AgentOptions["beforeCompaction"];
101
103
  /** Stable portable state identity. It cannot change after first construction or import. */