@agent-assembly/sdk 0.0.1-beta.3 → 0.0.1-beta.5

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 (75) hide show
  1. package/README.md +36 -23
  2. package/dist/cjs/core/gateway-resolver.js +73 -3
  3. package/dist/cjs/core/init-assembly.js +155 -33
  4. package/dist/cjs/core/redact.js +63 -0
  5. package/dist/cjs/gateway/client.js +63 -1
  6. package/dist/cjs/gateway/index.js +2 -1
  7. package/dist/cjs/hooks/ai-sdk.js +46 -10
  8. package/dist/cjs/hooks/langchain.js +12 -3
  9. package/dist/cjs/hooks/mastra.js +10 -6
  10. package/dist/cjs/hooks/openai-agents.js +1 -3
  11. package/dist/cjs/index.js +9 -1
  12. package/dist/cjs/native/client.js +94 -25
  13. package/dist/cjs/op-control.js +159 -17
  14. package/dist/cjs/runtime.js +73 -7
  15. package/dist/cjs/wrappers/with-assembly.js +89 -32
  16. package/dist/esm/core/gateway-resolver.js +72 -3
  17. package/dist/esm/core/gateway-resolver.js.map +1 -1
  18. package/dist/esm/core/init-assembly.js +154 -32
  19. package/dist/esm/core/init-assembly.js.map +1 -1
  20. package/dist/esm/core/redact.js +59 -0
  21. package/dist/esm/core/redact.js.map +1 -0
  22. package/dist/esm/gateway/client.js +62 -1
  23. package/dist/esm/gateway/client.js.map +1 -1
  24. package/dist/esm/gateway/index.js +1 -1
  25. package/dist/esm/gateway/index.js.map +1 -1
  26. package/dist/esm/hooks/ai-sdk.js +46 -10
  27. package/dist/esm/hooks/ai-sdk.js.map +1 -1
  28. package/dist/esm/hooks/langchain.js +12 -3
  29. package/dist/esm/hooks/langchain.js.map +1 -1
  30. package/dist/esm/hooks/mastra.js +10 -6
  31. package/dist/esm/hooks/mastra.js.map +1 -1
  32. package/dist/esm/hooks/openai-agents.js +1 -3
  33. package/dist/esm/hooks/openai-agents.js.map +1 -1
  34. package/dist/esm/index.js +6 -0
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/esm/native/client.js +91 -24
  37. package/dist/esm/native/client.js.map +1 -1
  38. package/dist/esm/op-control.js +124 -17
  39. package/dist/esm/op-control.js.map +1 -1
  40. package/dist/esm/runtime.js +72 -7
  41. package/dist/esm/runtime.js.map +1 -1
  42. package/dist/esm/wrappers/with-assembly.js +89 -32
  43. package/dist/esm/wrappers/with-assembly.js.map +1 -1
  44. package/dist/types/core/gateway-resolver.d.ts +18 -1
  45. package/dist/types/core/gateway-resolver.d.ts.map +1 -1
  46. package/dist/types/core/init-assembly.d.ts +2 -1
  47. package/dist/types/core/init-assembly.d.ts.map +1 -1
  48. package/dist/types/core/redact.d.ts +28 -0
  49. package/dist/types/core/redact.d.ts.map +1 -0
  50. package/dist/types/gateway/client.d.ts +17 -0
  51. package/dist/types/gateway/client.d.ts.map +1 -1
  52. package/dist/types/gateway/index.d.ts +1 -1
  53. package/dist/types/gateway/index.d.ts.map +1 -1
  54. package/dist/types/hooks/ai-sdk.d.ts +13 -0
  55. package/dist/types/hooks/ai-sdk.d.ts.map +1 -1
  56. package/dist/types/hooks/langchain.d.ts +11 -0
  57. package/dist/types/hooks/langchain.d.ts.map +1 -1
  58. package/dist/types/hooks/mastra.d.ts.map +1 -1
  59. package/dist/types/hooks/openai-agents.d.ts.map +1 -1
  60. package/dist/types/index.d.ts +4 -1
  61. package/dist/types/index.d.ts.map +1 -1
  62. package/dist/types/native/client.d.ts +44 -0
  63. package/dist/types/native/client.d.ts.map +1 -1
  64. package/dist/types/op-control.d.ts +71 -7
  65. package/dist/types/op-control.d.ts.map +1 -1
  66. package/dist/types/runtime.d.ts +27 -5
  67. package/dist/types/runtime.d.ts.map +1 -1
  68. package/dist/types/types/assembly-config.d.ts +6 -0
  69. package/dist/types/types/assembly-config.d.ts.map +1 -1
  70. package/dist/types/wrappers/index.d.ts +1 -1
  71. package/dist/types/wrappers/index.d.ts.map +1 -1
  72. package/dist/types/wrappers/with-assembly.d.ts +25 -0
  73. package/dist/types/wrappers/with-assembly.d.ts.map +1 -1
  74. package/native/aa-ffi-node/index.d.ts +82 -1
  75. package/package.json +18 -5
@@ -45,16 +45,15 @@ const agent_context_store_js_1 = require("../lineage/agent-context-store.js");
45
45
  exports.vercelAiSdkPatchState = {
46
46
  isPatched: false,
47
47
  originalToolFactory: undefined,
48
- patchedModule: undefined
48
+ patchedModule: undefined,
49
+ mutatedOriginal: false
49
50
  };
50
51
  function captureOriginalToolFactory(module) {
51
52
  const candidate = module.tool;
52
53
  if (typeof candidate !== "function") {
53
54
  return undefined;
54
55
  }
55
- if (!exports.vercelAiSdkPatchState.originalToolFactory) {
56
- exports.vercelAiSdkPatchState.originalToolFactory = candidate;
57
- }
56
+ exports.vercelAiSdkPatchState.originalToolFactory ??= candidate;
58
57
  return exports.vercelAiSdkPatchState.originalToolFactory;
59
58
  }
60
59
  function recordToolResultNonBlocking(gatewayClient, runId, output) {
@@ -76,7 +75,7 @@ function createWrappedExecute(originalExecute, description, gatewayClient, optio
76
75
  decision = await gatewayClient.check({
77
76
  action: "tool_call",
78
77
  toolName: description,
79
- args: args,
78
+ args,
80
79
  runId
81
80
  });
82
81
  }
@@ -114,6 +113,35 @@ function createPatchedToolFactory(originalToolFactory, gatewayClient, options) {
114
113
  };
115
114
  };
116
115
  }
116
+ /**
117
+ * Install `governed` as the module's `tool` factory without ever assigning to a
118
+ * frozen ESM namespace.
119
+ *
120
+ * A real `ai` package loaded via `import()` is an ES module: its namespace is an
121
+ * exotic object whose named exports are non-writable, so `module.tool = …` throws
122
+ * `Cannot assign to read only property 'tool'` (AAASM-3532). We therefore attempt
123
+ * the in-place assignment only as a fast path for writable plain objects (the
124
+ * shape used by the unit suite's `loadModule` fakes) and fall back to a mutable
125
+ * **shim copy** for the frozen-namespace case — the same `{ tool: aiModule.tool }`
126
+ * shim the AAASM-3525 integration driver proved works. The returned module is what
127
+ * downstream consumers read the governed factory from (`patchedModule.tool`).
128
+ */
129
+ function applyGovernedToolFactory(module, governed) {
130
+ if (Object.isExtensible(module)) {
131
+ try {
132
+ module.tool = governed;
133
+ return { patchedModule: module, mutatedOriginal: true };
134
+ }
135
+ catch {
136
+ // Some non-extensible-but-reported-extensible exotic objects still reject
137
+ // the write; fall through to the shim copy below.
138
+ }
139
+ }
140
+ return {
141
+ patchedModule: { ...module, tool: governed },
142
+ mutatedOriginal: false
143
+ };
144
+ }
117
145
  async function loadVercelAiSdkModule() {
118
146
  try {
119
147
  const moduleName = "ai";
@@ -137,13 +165,15 @@ async function patchVercelAiSdk(options) {
137
165
  if (!originalToolFactory) {
138
166
  return false;
139
167
  }
140
- module.tool = createPatchedToolFactory(originalToolFactory, options.gatewayClient, {
168
+ const governed = createPatchedToolFactory(originalToolFactory, options.gatewayClient, {
141
169
  approvalTimeoutMs: options.approvalTimeoutMs ?? 30_000,
142
170
  fallbackRunId: options.fallbackRunId ?? "vercel-ai-sdk",
143
- ...(options.agentId !== undefined ? { agentId: options.agentId } : {})
171
+ ...(options.agentId === undefined ? {} : { agentId: options.agentId })
144
172
  });
173
+ const { patchedModule, mutatedOriginal } = applyGovernedToolFactory(module, governed);
145
174
  exports.vercelAiSdkPatchState.isPatched = true;
146
- exports.vercelAiSdkPatchState.patchedModule = module;
175
+ exports.vercelAiSdkPatchState.patchedModule = patchedModule;
176
+ exports.vercelAiSdkPatchState.mutatedOriginal = mutatedOriginal;
147
177
  return true;
148
178
  }
149
179
  function unpatchVercelAiSdk() {
@@ -156,9 +186,15 @@ function unpatchVercelAiSdk() {
156
186
  if (!exports.vercelAiSdkPatchState.originalToolFactory) {
157
187
  return false;
158
188
  }
159
- exports.vercelAiSdkPatchState.patchedModule.tool =
160
- exports.vercelAiSdkPatchState.originalToolFactory;
189
+ // Only restore when we mutated a writable module in place. For the frozen-ESM
190
+ // shim path the original `ai` namespace was never touched, so there is nothing
191
+ // to write back — and attempting it would re-throw the AAASM-3532 crash.
192
+ if (exports.vercelAiSdkPatchState.mutatedOriginal) {
193
+ exports.vercelAiSdkPatchState.patchedModule.tool =
194
+ exports.vercelAiSdkPatchState.originalToolFactory;
195
+ }
161
196
  exports.vercelAiSdkPatchState.isPatched = false;
162
197
  exports.vercelAiSdkPatchState.patchedModule = undefined;
198
+ exports.vercelAiSdkPatchState.mutatedOriginal = false;
163
199
  return true;
164
200
  }
@@ -1,9 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.patchLangChain = patchLangChain;
4
+ /**
5
+ * Intentional no-op stub: native-transport LangChain patching is not
6
+ * implemented. LangChain enforcement is performed in the SDK's callback layer
7
+ * (post-execution redaction) and wrapper layer (pre-execution deny) wired by
8
+ * `initAssembly`, not through this native hook — so there is nothing to patch
9
+ * here yet. Returns `false` (nothing patched) for every mode.
10
+ *
11
+ * The `client` parameter is retained to keep the adapter-registry hook
12
+ * signature (and the public `patchLangChain` export) uniform with the other
13
+ * `patch*` hooks; it is deliberately unused until native patching lands.
14
+ */
15
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- stub param kept for signature stability; see TSDoc above
4
16
  async function patchLangChain(client) {
5
- if (client.mode === "grpc-sidecar" || client.mode === "napi-inprocess") {
6
- return false;
7
- }
8
17
  return false;
9
18
  }
@@ -75,15 +75,18 @@ async function patchMastra(options) {
75
75
  exports.mastraPatchState.originalGenerate = originalGenerate;
76
76
  exports.mastraPatchState.patchedAgentClass = module.Agent;
77
77
  module.Agent.prototype.generate = function patchedGenerate(...args) {
78
- let result;
78
+ // The try guards only the *synchronous* setup (entering the async-context
79
+ // store and invoking the original); the returned promise is handed to the
80
+ // caller, which awaits it, so its rejection is the caller's to handle.
81
+ // Returning it directly (rather than via a local inside the try) avoids an
82
+ // unhandled-rejection footgun without changing timing or call count.
79
83
  try {
80
- result = (0, agent_context_store_js_1.runWithAgentId)(agentId, () => originalGenerate.apply(this, args));
84
+ return (0, agent_context_store_js_1.runWithAgentId)(agentId, () => originalGenerate.apply(this, args));
81
85
  }
82
86
  catch (e) {
83
87
  console.warn("[assembly] Mastra lineage patch error on generate; falling back:", e);
84
88
  return originalGenerate.apply(this, args);
85
89
  }
86
- return result;
87
90
  };
88
91
  // Wrap Workflow.prototype.execute if present
89
92
  if (module.Workflow?.prototype?.execute) {
@@ -91,15 +94,16 @@ async function patchMastra(options) {
91
94
  exports.mastraPatchState.originalExecute = originalExecute;
92
95
  exports.mastraPatchState.patchedWorkflowClass = module.Workflow;
93
96
  module.Workflow.prototype.execute = function patchedExecute(...args) {
94
- let result;
97
+ // See patchedGenerate above: the try guards only the synchronous setup;
98
+ // the returned promise is awaited by the caller, so returning it directly
99
+ // (not via a local) handles its rejection without altering behaviour.
95
100
  try {
96
- result = (0, agent_context_store_js_1.runWithAgentId)(agentId, () => originalExecute.apply(this, args));
101
+ return (0, agent_context_store_js_1.runWithAgentId)(agentId, () => originalExecute.apply(this, args));
97
102
  }
98
103
  catch (e) {
99
104
  console.warn("[assembly] Mastra lineage patch error on execute; falling back:", e);
100
105
  return originalExecute.apply(this, args);
101
106
  }
102
- return result;
103
107
  };
104
108
  }
105
109
  exports.mastraPatchState.isPatched = true;
@@ -54,9 +54,7 @@ function captureOriginalRunTool(agentClass) {
54
54
  if (!candidate) {
55
55
  return undefined;
56
56
  }
57
- if (!exports.openAIAgentsPatchState.originalRunTool) {
58
- exports.openAIAgentsPatchState.originalRunTool = candidate;
59
- }
57
+ exports.openAIAgentsPatchState.originalRunTool ??= candidate;
60
58
  return exports.openAIAgentsPatchState.originalRunTool;
61
59
  }
62
60
  function parseToolCallArguments(toolCall) {
package/dist/cjs/index.js CHANGED
@@ -14,11 +14,19 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.PolicyViolationError = exports.createNoopGatewayClient = exports.findAasmBinary = exports.INSTALL_HINT = exports.runWithAgentId = exports.currentAgentId = exports.encodeCallStackNode = exports.encodeAuditEvent = exports.decodeCallStackNode = exports.decodeAuditEvent = exports.ENFORCEMENT_MODES = exports.withAssembly = exports.initAssembly = void 0;
17
+ exports.PolicyViolationError = exports.createNoopGatewayClient = exports.findAasmBinary = exports.INSTALL_HINT = exports.runWithAgentId = exports.currentAgentId = exports.encodeCallStackNode = exports.encodeAuditEvent = exports.decodeCallStackNode = exports.decodeAuditEvent = exports.ENFORCEMENT_MODES = exports.OpTerminatedError = exports.OpControlSubscriber = exports.withAssembly = exports.initAssembly = void 0;
18
18
  var init_assembly_js_1 = require("./core/init-assembly.js");
19
19
  Object.defineProperty(exports, "initAssembly", { enumerable: true, get: function () { return init_assembly_js_1.initAssembly; } });
20
20
  var index_js_1 = require("./wrappers/index.js");
21
21
  Object.defineProperty(exports, "withAssembly", { enumerable: true, get: function () { return index_js_1.withAssembly; } });
22
+ // Live op-control consumer (AAASM-3491). Subscribes to the gateway's
23
+ // OpControlStream and exposes per-op cooperative-pause / fast-fail-terminate;
24
+ // pass the subscriber as `withAssembly(..., { opControl })` so an operator
25
+ // terminate/pause reaches a running tool through the SDK tool path.
26
+ var op_control_js_1 = require("./op-control.js");
27
+ Object.defineProperty(exports, "OpControlSubscriber", { enumerable: true, get: function () { return op_control_js_1.OpControlSubscriber; } });
28
+ var op_terminated_error_js_1 = require("./errors/op-terminated-error.js");
29
+ Object.defineProperty(exports, "OpTerminatedError", { enumerable: true, get: function () { return op_terminated_error_js_1.OpTerminatedError; } });
22
30
  var index_js_2 = require("./types/index.js");
23
31
  Object.defineProperty(exports, "ENFORCEMENT_MODES", { enumerable: true, get: function () { return index_js_2.ENFORCEMENT_MODES; } });
24
32
  var index_js_3 = require("./audit/index.js");
@@ -3,14 +3,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.NativeDisconnectError = exports.NativeQueryPolicyError = exports.NativeSendEventError = exports.NativeConnectError = void 0;
6
+ exports.NativeDisconnectError = exports.NativeRegisterError = exports.NativeQueryPolicyError = exports.NativeSendEventError = exports.NativeConnectError = void 0;
7
+ exports.resolveSdkVersion = resolveSdkVersion;
7
8
  exports.createNativeClient = createNativeClient;
8
9
  const node_module_1 = require("node:module");
9
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ /**
12
+ * Resolve the installed `@agent-assembly/sdk` package version, or `undefined`.
13
+ *
14
+ * Forwarded into the native `connect` so the user-facing npm package version —
15
+ * not the shared `aa-sdk-client` crate version — is what gets signed into the
16
+ * runtime handshake, giving accurate downgrade detection (AAASM-3683).
17
+ * `undefined` lets the native shim fall back to the crate version (no
18
+ * regression vs AAASM-3666). Uses `createRequire(<cwd>/package.json)`, the same
19
+ * ESM/CJS-safe pattern as the native-binding and runtime-binary resolvers.
20
+ */
21
+ function resolveSdkVersion() {
22
+ try {
23
+ const requireFromHere = (0, node_module_1.createRequire)(node_path_1.default.resolve(process.cwd(), "package.json"));
24
+ const pkg = requireFromHere("@agent-assembly/sdk/package.json");
25
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
10
31
  const NATIVE_BINDING_SINGLETON_KEY = Symbol.for("@agent-assembly/sdk/native-binding");
11
32
  const ERROR_CONNECT = "AA_ERR_CONNECT";
12
33
  const ERROR_SEND_EVENT = "AA_ERR_SEND_EVENT";
13
34
  const ERROR_QUERY_POLICY = "AA_ERR_QUERY_POLICY";
35
+ const ERROR_REGISTER = "AA_ERR_REGISTER";
14
36
  const ERROR_DISCONNECT = "AA_ERR_DISCONNECT";
15
37
  class NativeConnectError extends Error {
16
38
  code = ERROR_CONNECT;
@@ -24,10 +46,33 @@ class NativeQueryPolicyError extends Error {
24
46
  code = ERROR_QUERY_POLICY;
25
47
  }
26
48
  exports.NativeQueryPolicyError = NativeQueryPolicyError;
49
+ class NativeRegisterError extends Error {
50
+ code = ERROR_REGISTER;
51
+ }
52
+ exports.NativeRegisterError = NativeRegisterError;
27
53
  class NativeDisconnectError extends Error {
28
54
  code = ERROR_DISCONNECT;
29
55
  }
30
56
  exports.NativeDisconnectError = NativeDisconnectError;
57
+ /**
58
+ * Translate the native `{decision, reason}` verdict into the SDK's
59
+ * `PolicyResult`. Only `"deny"` blocks; `"pending"` routes to the approval
60
+ * path; `"allow"` / `"redact"` / any unrecognized value proceed. This mirrors
61
+ * the shared enforcement contract across the Python / Go / Node SDKs.
62
+ *
63
+ * The native primitive already fails open (returns `"allow"`) when the runtime
64
+ * is unreachable or too slow, so a missing or degraded runtime never blocks.
65
+ */
66
+ function mapDecisionToPolicyResult(verdict) {
67
+ switch (verdict.decision) {
68
+ case "deny":
69
+ return { denied: true, pending: false, reason: verdict.reason };
70
+ case "pending":
71
+ return { denied: false, pending: true, reason: verdict.reason };
72
+ default:
73
+ return { denied: false, pending: false };
74
+ }
75
+ }
31
76
  function mapNativeError(error) {
32
77
  if (!(error instanceof Error)) {
33
78
  return new Error(String(error));
@@ -43,6 +88,9 @@ function mapNativeError(error) {
43
88
  if (code === ERROR_QUERY_POLICY) {
44
89
  return new NativeQueryPolicyError(detail);
45
90
  }
91
+ if (code === ERROR_REGISTER) {
92
+ return new NativeRegisterError(detail);
93
+ }
46
94
  if (code === ERROR_DISCONNECT) {
47
95
  return new NativeDisconnectError(detail);
48
96
  }
@@ -51,9 +99,7 @@ function mapNativeError(error) {
51
99
  function loadNativeBinding() {
52
100
  const shouldUseCache = process.env.VITEST !== "true";
53
101
  const globalObject = globalThis;
54
- const cachedBinding = shouldUseCache
55
- ? globalObject[NATIVE_BINDING_SINGLETON_KEY]
56
- : undefined;
102
+ const cachedBinding = shouldUseCache ? globalObject[NATIVE_BINDING_SINGLETON_KEY] : undefined;
57
103
  if (cachedBinding) {
58
104
  return cachedBinding;
59
105
  }
@@ -85,7 +131,11 @@ function createNativeClient(options) {
85
131
  mode,
86
132
  close: async () => undefined,
87
133
  sendEvent: () => undefined,
88
- queryPolicy: async () => ({ denied: false, pending: false })
134
+ queryPolicy: async () => ({ denied: false, pending: false }),
135
+ // No native session to register against off the in-process path; the
136
+ // gRPC sidecar registers the agent in its own process. Resolve neutrally
137
+ // so init never blocks on a transport that does not own a handle.
138
+ register: async () => ""
89
139
  };
90
140
  }
91
141
  const binding = loadNativeBinding();
@@ -93,20 +143,21 @@ function createNativeClient(options) {
93
143
  let handlePromise;
94
144
  let activeHandle;
95
145
  let pendingSendError;
146
+ const sdkVersion = resolveSdkVersion();
96
147
  const getHandle = async () => {
97
- if (!handlePromise) {
98
- handlePromise = binding
99
- .connect(socketPath)
100
- .then((handle) => {
101
- activeHandle = handle;
102
- return handle;
103
- })
104
- .catch((error) => {
105
- handlePromise = undefined;
106
- activeHandle = undefined;
107
- throw mapNativeError(error);
108
- });
109
- }
148
+ handlePromise ??= binding
149
+ // Forward the npm package version so it is signed into the handshake
150
+ // (AAASM-3683); agent id is wired at register time, not connect.
151
+ .connect(socketPath, undefined, sdkVersion)
152
+ .then((handle) => {
153
+ activeHandle = handle;
154
+ return handle;
155
+ })
156
+ .catch((error) => {
157
+ handlePromise = undefined;
158
+ activeHandle = undefined;
159
+ throw mapNativeError(error);
160
+ });
110
161
  return handlePromise;
111
162
  };
112
163
  return {
@@ -145,18 +196,36 @@ function createNativeClient(options) {
145
196
  pendingSendError = mapNativeError(error);
146
197
  });
147
198
  },
148
- queryPolicy: async () => {
199
+ queryPolicy: async (action) => {
149
200
  if (pendingSendError) {
150
201
  const error = pendingSendError;
151
202
  pendingSendError = undefined;
152
203
  throw error;
153
204
  }
154
- // The SDK is not a policy authority. Ensure the session is connected
155
- // (surfacing any connect error), then defer: authoritative policy and
156
- // approval are enforced server-side (gateway / runtime). The native shim
157
- // no longer synthesizes a decision, so this always resolves neutral.
158
- await getHandle();
159
- return { denied: false, pending: false };
205
+ // Connect (surfacing any connect error as a genuine local fault), then
206
+ // ask the runtime for an authoritative verdict via the native primitive.
207
+ // The native `queryPolicy` is async it offloads its blocking wait to a
208
+ // worker thread, so awaiting it never blocks the Node event loop — and it
209
+ // already fails open (returns `"allow"`) when the runtime is unreachable
210
+ // or too slow, so a missing or degraded runtime never blocks the agent.
211
+ const handle = await getHandle();
212
+ const verdict = await binding.queryPolicy(handle, action);
213
+ return mapDecisionToPolicyResult(verdict);
214
+ },
215
+ register: async (options) => {
216
+ // Register on the same session the queryPolicy path uses, so the token
217
+ // the gateway issues is stored on this handle and attached to every
218
+ // subsequent query. This is the only direct SDK→gateway gRPC call
219
+ // (ADR 0004); CheckAction still flows through aa-runtime.
220
+ if (binding.register === undefined) {
221
+ // A binding without `register` predates AAASM-3400; the agent simply
222
+ // runs unregistered rather than failing init.
223
+ return "";
224
+ }
225
+ const handle = await getHandle();
226
+ return binding.register(handle, options).catch((error) => {
227
+ throw mapNativeError(error);
228
+ });
160
229
  }
161
230
  };
162
231
  }
@@ -20,39 +20,178 @@
20
20
  * - Auto-wiring into the existing `GatewayClient` / adapter hooks
21
21
  * (separate sub-task when the adapter surface is stable).
22
22
  */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
23
56
  Object.defineProperty(exports, "__esModule", { value: true });
24
57
  exports.OpControlSubscriber = void 0;
25
- const grpc_js_1 = require("@grpc/grpc-js");
58
+ exports.gatewayHostOf = gatewayHostOf;
59
+ exports.resolveOpControlCredentials = resolveOpControlCredentials;
26
60
  const op_terminated_error_js_1 = require("./errors/op-terminated-error.js");
27
- const policy_js_1 = require("./proto/generated/policy.js");
61
+ /**
62
+ * Numeric `OpControlSignal` values, inlined to keep this module grpc-free at load.
63
+ *
64
+ * `OpControlSignal` lives in `./proto/generated/policy.js`, which imports
65
+ * `@grpc/grpc-js` at module scope; importing the enum as a *value* would defeat
66
+ * the lazy-load. These are the stable protobuf wire numbers (UNSPECIFIED=0,
67
+ * PAUSE=1, RESUME=2, TERMINATE=3) — `msg.signal` is compared against them
68
+ * numerically in {@link OpControlSubscriber.dispatch}. The `OpControlSignal`
69
+ * type is still imported (type-only) for signatures.
70
+ */
71
+ const SIGNAL_PAUSE = 1;
72
+ const SIGNAL_RESUME = 2;
73
+ const SIGNAL_TERMINATE = 3;
74
+ /**
75
+ * Hosts treated as loopback for the secure-by-default transport decision.
76
+ * A loopback gateway is the local dev-mode CP, where plaintext gRPC is the
77
+ * documented default; anything else is presumed remote and must be encrypted.
78
+ */
79
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
80
+ /**
81
+ * Extract the bare host from a gRPC target (`host:port`, a bare host, or a
82
+ * URL-style `scheme://host:port`). Returns the lowercased host with any
83
+ * surrounding IPv6 brackets preserved so it can be matched against
84
+ * {@link LOOPBACK_HOSTS}.
85
+ */
86
+ function gatewayHostOf(gatewayUrl) {
87
+ let target = gatewayUrl.trim();
88
+ const schemeIdx = target.indexOf("://");
89
+ if (schemeIdx !== -1)
90
+ target = target.slice(schemeIdx + 3);
91
+ // Drop a path/query suffix if a URL form was passed.
92
+ const slashIdx = target.indexOf("/");
93
+ if (slashIdx !== -1)
94
+ target = target.slice(0, slashIdx);
95
+ if (target.startsWith("[")) {
96
+ // Bracketed IPv6: keep the bracketed form, strip only the trailing :port.
97
+ const close = target.indexOf("]");
98
+ return close === -1 ? target.toLowerCase() : target.slice(0, close + 1).toLowerCase();
99
+ }
100
+ const colonIdx = target.indexOf(":");
101
+ if (colonIdx !== -1)
102
+ target = target.slice(0, colonIdx);
103
+ return target.toLowerCase();
104
+ }
105
+ function isLoopbackTarget(gatewayUrl) {
106
+ return LOOPBACK_HOSTS.has(gatewayHostOf(gatewayUrl));
107
+ }
108
+ /**
109
+ * Pick channel credentials for the op-control stream, secure by default.
110
+ *
111
+ * Precedence: an explicit `credentials` override wins; otherwise a loopback
112
+ * target gets plaintext (local dev gateway), a remote target gets TLS, and a
113
+ * remote target is only allowed plaintext when the caller sets `allowInsecure`.
114
+ *
115
+ * `grpcCredentials` is injected (rather than imported at module scope) so this
116
+ * module does not eagerly load `@grpc/grpc-js` — see the module header. The
117
+ * real-connect path passes the lazily-imported `credentials` namespace.
118
+ *
119
+ * @throws never — returns the chosen {@link ChannelCredentials}.
120
+ */
121
+ function resolveOpControlCredentials(gatewayUrl, opts, grpcCredentials) {
122
+ if (opts.credentials)
123
+ return opts.credentials;
124
+ if (isLoopbackTarget(gatewayUrl))
125
+ return grpcCredentials.createInsecure();
126
+ if (opts.allowInsecure)
127
+ return grpcCredentials.createInsecure();
128
+ return grpcCredentials.createSsl();
129
+ }
28
130
  class OpControlSubscriber {
29
- client;
131
+ /**
132
+ * `null` until the channel is opened. On the test-seam (`clientFactory`) path
133
+ * it is set synchronously in {@link connect}; on the real-connect path it is
134
+ * set asynchronously once `@grpc/grpc-js` + `PolicyServiceClient` have been
135
+ * lazily imported (see {@link openRealChannel}).
136
+ */
137
+ client = null;
30
138
  agent;
31
139
  ops = new Map();
32
140
  call = null;
33
141
  alive = true;
34
- constructor(client, agent) {
35
- this.client = client;
142
+ /** Set once {@link close} is called before the async channel finishes opening. */
143
+ closed = false;
144
+ constructor(agent) {
36
145
  this.agent = agent;
37
146
  }
38
- /** Open the gRPC channel + subscription stream and start the reader. */
147
+ /**
148
+ * Open the gRPC channel + subscription stream and start the reader.
149
+ *
150
+ * Returns synchronously. On the real-connect path the channel is opened
151
+ * asynchronously — `@grpc/grpc-js` and `PolicyServiceClient` are loaded lazily
152
+ * (`await import`) so that importing this module never eagerly pulls grpc (see
153
+ * the module header). The test seam (`clientFactory`) opens synchronously and
154
+ * never touches grpc.
155
+ */
39
156
  static connect(gatewayUrl, opts) {
40
157
  const agent = {
41
158
  orgId: opts.orgId,
42
159
  teamId: opts.teamId,
43
- agentId: opts.agentId,
160
+ agentId: opts.agentId
44
161
  };
45
- const client = opts.clientFactory
46
- ? opts.clientFactory()
47
- : new policy_js_1.PolicyServiceClient(gatewayUrl, opts.credentials ?? grpc_js_1.credentials.createInsecure());
48
- const subscriber = new OpControlSubscriber(client, agent);
49
- subscriber.start();
162
+ const subscriber = new OpControlSubscriber(agent);
163
+ if (opts.clientFactory) {
164
+ subscriber.client = opts.clientFactory();
165
+ subscriber.start();
166
+ }
167
+ else {
168
+ // Real channel: defer grpc loading to the dynamic-import path. Errors
169
+ // surface as a dead stream so callers see `streamAlive() === false`.
170
+ void subscriber.openRealChannel(gatewayUrl, opts).catch(() => {
171
+ subscriber.markStreamDead();
172
+ });
173
+ }
50
174
  return subscriber;
51
175
  }
176
+ /**
177
+ * Lazily import grpc + the policy client, build the real client, and start the
178
+ * reader. Kept off the module's import graph so `import '@agent-assembly/sdk'`
179
+ * stays grpc-free until a subscriber actually opens a live channel.
180
+ */
181
+ async openRealChannel(gatewayUrl, opts) {
182
+ const { credentials } = await Promise.resolve().then(() => __importStar(require("@grpc/grpc-js")));
183
+ const { PolicyServiceClient } = await Promise.resolve().then(() => __importStar(require("./proto/generated/policy.js")));
184
+ if (this.closed)
185
+ return; // close() raced ahead of the async open.
186
+ this.client = new PolicyServiceClient(gatewayUrl, resolveOpControlCredentials(gatewayUrl, opts, credentials));
187
+ this.start();
188
+ }
52
189
  /** Open the stream and wire reader handlers. Public so tests can call
53
190
  * directly after constructing with a hand-rolled client.
54
191
  */
55
192
  start() {
193
+ if (!this.client)
194
+ return;
56
195
  this.call = this.client.opControlStream({ agentId: this.agent });
57
196
  this.call.on("data", (msg) => this.dispatch(msg));
58
197
  this.call.on("error", () => this.markStreamDead());
@@ -61,14 +200,14 @@ class OpControlSubscriber {
61
200
  dispatch(msg) {
62
201
  const state = this.slot(msg.opId);
63
202
  switch (msg.signal) {
64
- case policy_js_1.OpControlSignal.OP_CONTROL_SIGNAL_PAUSE:
203
+ case SIGNAL_PAUSE:
65
204
  state.paused = true;
66
205
  break;
67
- case policy_js_1.OpControlSignal.OP_CONTROL_SIGNAL_RESUME:
206
+ case SIGNAL_RESUME:
68
207
  state.paused = false;
69
208
  this.flushResolvers(state);
70
209
  break;
71
- case policy_js_1.OpControlSignal.OP_CONTROL_SIGNAL_TERMINATE:
210
+ case SIGNAL_TERMINATE:
72
211
  state.terminated = true;
73
212
  this.flushResolvers(state);
74
213
  break;
@@ -140,10 +279,13 @@ class OpControlSubscriber {
140
279
  streamAlive() {
141
280
  return this.alive;
142
281
  }
143
- /** Cancel the stream and clean up. */
282
+ /** Cancel the stream and clean up. Safe to call before the async real-channel
283
+ * open has completed — it flags `closed` so the pending open bails out.
284
+ */
144
285
  close() {
286
+ this.closed = true;
145
287
  this.call?.cancel();
146
- this.client.close?.();
288
+ this.client?.close?.();
147
289
  this.markStreamDead();
148
290
  }
149
291
  }