@dudousxd/nestjs-agent-core 0.35.0 → 0.37.0

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.
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ var AGENT_APPROVAL_PORT = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:app
27
27
  var AGENT_SKILLS = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:skills");
28
28
  var AGENT_MEMORY = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:memory");
29
29
  var AGENT_SKILL_SOURCES = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:skill-sources");
30
+ var AGENT_CONFIRM_TOKEN_STORE = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:confirm-token-store");
30
31
 
31
32
  // src/spi/model-catalog.ts
32
33
  function staticModelCatalog(view) {
@@ -2690,15 +2691,40 @@ var DefaultRolesPolicy = class {
2690
2691
  __name(this, "DefaultRolesPolicy");
2691
2692
  }
2692
2693
  defaultRoles;
2693
- constructor(defaultRoles = []) {
2694
+ /** What an empty roles list means here — see {@link EmptyRoles}. */
2695
+ emptyRoles;
2696
+ constructor(defaultRoles = [], options = {}) {
2694
2697
  this.defaultRoles = defaultRoles;
2698
+ this.emptyRoles = options.emptyRoles ?? "allow";
2695
2699
  }
2696
2700
  can(actor, tool) {
2697
2701
  const allowed = tool.roles ?? this.defaultRoles;
2698
- if (allowed.length === 0) return true;
2702
+ if (allowed.length === 0) return this.emptyRoles === "allow";
2699
2703
  return (actor.roles ?? []).some((role) => allowed.includes(role));
2700
2704
  }
2701
2705
  };
2706
+ var ClosedRolesPolicy = class extends DefaultRolesPolicy {
2707
+ static {
2708
+ __name(this, "ClosedRolesPolicy");
2709
+ }
2710
+ constructor(defaultRoles = []) {
2711
+ super(defaultRoles, {
2712
+ emptyRoles: "deny"
2713
+ });
2714
+ }
2715
+ };
2716
+ function closeEmptyRoles(inner) {
2717
+ const defaults = inner instanceof DefaultRolesPolicy ? inner.defaultRoles : [];
2718
+ return {
2719
+ can(actor, tool) {
2720
+ if (tool.ability === void 0 && (tool.roles ?? defaults).length === 0) {
2721
+ return false;
2722
+ }
2723
+ return inner.can(actor, tool);
2724
+ }
2725
+ };
2726
+ }
2727
+ __name(closeEmptyRoles, "closeEmptyRoles");
2702
2728
 
2703
2729
  // src/agent-loop.ts
2704
2730
  import { createHash } from "crypto";
@@ -5762,11 +5788,321 @@ var InMemoryAgentStore = class {
5762
5788
  };
5763
5789
  }
5764
5790
  };
5791
+
5792
+ // src/confirm-token.ts
5793
+ import { createHash as createHash2, createHmac, timingSafeEqual } from "crypto";
5794
+ var DEFAULT_CONFIRM_TTL_MS = 15 * 60 * 1e3;
5795
+ function canonicalJson(value) {
5796
+ if (value === void 0) return "null";
5797
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
5798
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
5799
+ if (typeof value.toJSON === "function") {
5800
+ return canonicalJson(value.toJSON());
5801
+ }
5802
+ const entries = Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
5803
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
5804
+ }
5805
+ __name(canonicalJson, "canonicalJson");
5806
+ function signature(subject, expiresAt, secret) {
5807
+ if (secret === "") {
5808
+ throw new Error("confirm token: the secret is empty");
5809
+ }
5810
+ return createHmac("sha256", secret).update(canonicalJson([
5811
+ "agent-confirm",
5812
+ subject.tool,
5813
+ subject.actorId,
5814
+ subject.tenantRef ?? "",
5815
+ expiresAt,
5816
+ subject.args
5817
+ ])).digest("base64url");
5818
+ }
5819
+ __name(signature, "signature");
5820
+ function signConfirmToken(subject, options) {
5821
+ return `${options.expiresAt}.${signature(subject, options.expiresAt, options.secret)}`;
5822
+ }
5823
+ __name(signConfirmToken, "signConfirmToken");
5824
+ function confirmTokenExpiry(token) {
5825
+ const separator = token.indexOf(".");
5826
+ if (separator <= 0) return void 0;
5827
+ const expiresAt = Number(token.slice(0, separator));
5828
+ return Number.isSafeInteger(expiresAt) && expiresAt > 0 ? expiresAt : void 0;
5829
+ }
5830
+ __name(confirmTokenExpiry, "confirmTokenExpiry");
5831
+ function verifyConfirmToken(token, subject, options) {
5832
+ if (typeof token !== "string") return false;
5833
+ const expiresAt = confirmTokenExpiry(token);
5834
+ if (expiresAt === void 0 || expiresAt < (options.now ?? Date.now())) return false;
5835
+ const expected = Buffer.from(signature(subject, expiresAt, options.secret));
5836
+ const given = Buffer.from(token.slice(token.indexOf(".") + 1));
5837
+ return expected.length === given.length && timingSafeEqual(expected, given);
5838
+ }
5839
+ __name(verifyConfirmToken, "verifyConfirmToken");
5840
+ function hashConfirmToken(token) {
5841
+ return createHash2("sha256").update(token).digest("hex");
5842
+ }
5843
+ __name(hashConfirmToken, "hashConfirmToken");
5844
+ var InMemoryConfirmTokenStore = class {
5845
+ static {
5846
+ __name(this, "InMemoryConfirmTokenStore");
5847
+ }
5848
+ claims = /* @__PURE__ */ new Map();
5849
+ async claim(input) {
5850
+ await this.purgeExpired();
5851
+ if (this.claims.has(input.hash)) return false;
5852
+ this.claims.set(input.hash, {
5853
+ ...input
5854
+ });
5855
+ return true;
5856
+ }
5857
+ async release(hash) {
5858
+ this.claims.delete(hash);
5859
+ }
5860
+ async purgeExpired(now = Date.now()) {
5861
+ let purged = 0;
5862
+ for (const [hash, claim] of this.claims) {
5863
+ if (claim.expiresAt < now) {
5864
+ this.claims.delete(hash);
5865
+ purged += 1;
5866
+ }
5867
+ }
5868
+ return purged;
5869
+ }
5870
+ /** How many tokens are currently marked as spent. */
5871
+ get size() {
5872
+ return this.claims.size;
5873
+ }
5874
+ };
5875
+
5876
+ // src/confirmed-tool.ts
5877
+ var DEFAULT_MESSAGES = {
5878
+ confirm: 'Nothing was written. Show this preview to the user and ask them to confirm. To commit, call this tool again with the SAME arguments plus "confirm": true and this "confirmToken" (valid for {minutes} minutes).',
5879
+ invalid: 'Invalid or expired confirmation: nothing was written. Call the tool without "confirm" to get a fresh preview, then confirm with the "confirmToken" it returns, without changing the arguments.',
5880
+ used: "This preview was already confirmed: nothing was written again. To repeat the operation, ask for a new preview."
5881
+ };
5882
+ var ConfirmTokenError = class extends Error {
5883
+ static {
5884
+ __name(this, "ConfirmTokenError");
5885
+ }
5886
+ reason;
5887
+ constructor(reason, message) {
5888
+ super(message), this.reason = reason;
5889
+ this.name = "ConfirmTokenError";
5890
+ }
5891
+ };
5892
+ var CONFIRM_JSON_SCHEMA_PROPERTIES = {
5893
+ confirm: {
5894
+ type: "boolean",
5895
+ description: "Omit to get a preview (nothing is written). Send true, with confirmToken, to commit."
5896
+ },
5897
+ confirmToken: {
5898
+ type: "string",
5899
+ description: "The confirmToken the preview returned. Required when confirm is true."
5900
+ }
5901
+ };
5902
+ var SCHEMA_EXTENSION = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent:schema-extension");
5903
+ function schemaExtensionOf(schema) {
5904
+ const extension = schema[SCHEMA_EXTENSION];
5905
+ return typeof extension === "object" && extension !== null ? extension : void 0;
5906
+ }
5907
+ __name(schemaExtensionOf, "schemaExtensionOf");
5908
+ function isRecord(value) {
5909
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5910
+ }
5911
+ __name(isRecord, "isRecord");
5912
+ function splitConfirm(input) {
5913
+ if (!isRecord(input)) return {
5914
+ args: input,
5915
+ fields: {}
5916
+ };
5917
+ const { confirm, confirmToken, ...args } = input;
5918
+ return {
5919
+ args,
5920
+ fields: {
5921
+ ...typeof confirm === "boolean" ? {
5922
+ confirm
5923
+ } : {},
5924
+ ...typeof confirmToken === "string" ? {
5925
+ confirmToken
5926
+ } : {}
5927
+ }
5928
+ };
5929
+ }
5930
+ __name(splitConfirm, "splitConfirm");
5931
+ function jsonSchemaInputOf(schema) {
5932
+ const standard = schema["~standard"];
5933
+ const input = standard.jsonSchema?.input;
5934
+ return typeof input === "function" ? (options) => input.call(standard.jsonSchema, options) : void 0;
5935
+ }
5936
+ __name(jsonSchemaInputOf, "jsonSchemaInputOf");
5937
+ function withConfirmFields(schema) {
5938
+ const inner = schema["~standard"];
5939
+ const innerJsonSchema = jsonSchemaInputOf(schema);
5940
+ const describe = /* @__PURE__ */ __name((options) => {
5941
+ const base = innerJsonSchema?.(options) ?? {
5942
+ additionalProperties: true
5943
+ };
5944
+ return {
5945
+ ...base,
5946
+ type: "object",
5947
+ properties: {
5948
+ ...isRecord(base.properties) ? base.properties : {},
5949
+ ...CONFIRM_JSON_SCHEMA_PROPERTIES
5950
+ }
5951
+ };
5952
+ }, "describe");
5953
+ const wrapped = {
5954
+ [SCHEMA_EXTENSION]: {
5955
+ base: schema,
5956
+ properties: CONFIRM_JSON_SCHEMA_PROPERTIES
5957
+ },
5958
+ "~standard": {
5959
+ version: 1,
5960
+ vendor: "@dudousxd/nestjs-agent",
5961
+ validate: /* @__PURE__ */ __name(async (value) => {
5962
+ if (!isRecord(value)) {
5963
+ return {
5964
+ issues: [
5965
+ {
5966
+ message: "expected an object"
5967
+ }
5968
+ ]
5969
+ };
5970
+ }
5971
+ const issues = [];
5972
+ if (value.confirm != null && typeof value.confirm !== "boolean") {
5973
+ issues.push({
5974
+ message: "expected a boolean",
5975
+ path: [
5976
+ "confirm"
5977
+ ]
5978
+ });
5979
+ }
5980
+ if (value.confirmToken != null && typeof value.confirmToken !== "string") {
5981
+ issues.push({
5982
+ message: "expected a string",
5983
+ path: [
5984
+ "confirmToken"
5985
+ ]
5986
+ });
5987
+ }
5988
+ const { args, fields } = splitConfirm(value);
5989
+ const result = await inner.validate(args);
5990
+ if (result.issues !== void 0) {
5991
+ return {
5992
+ issues: [
5993
+ ...issues,
5994
+ ...result.issues
5995
+ ]
5996
+ };
5997
+ }
5998
+ if (issues.length > 0) {
5999
+ return {
6000
+ issues
6001
+ };
6002
+ }
6003
+ return {
6004
+ value: isRecord(result.value) ? {
6005
+ ...result.value,
6006
+ ...fields
6007
+ } : result.value
6008
+ };
6009
+ }, "validate"),
6010
+ jsonSchema: {
6011
+ input: describe,
6012
+ output: describe
6013
+ }
6014
+ }
6015
+ };
6016
+ return wrapped;
6017
+ }
6018
+ __name(withConfirmFields, "withConfirmFields");
6019
+ function defineConfirmedTool(options, steps) {
6020
+ const { secret, ttlMs = DEFAULT_CONFIRM_TTL_MS, store, messages, input, ...specFields } = options;
6021
+ const text = {
6022
+ ...DEFAULT_MESSAGES,
6023
+ ...messages
6024
+ };
6025
+ const howToConfirm = text.confirm.replaceAll("{minutes}", String(Math.round(ttlMs / 6e4)));
6026
+ const secretNow = /* @__PURE__ */ __name(() => typeof secret === "function" ? secret() : secret, "secretNow");
6027
+ const handler = {
6028
+ async execute(value, ctx) {
6029
+ const { args, fields } = splitConfirm(value);
6030
+ const subject = {
6031
+ tool: options.name,
6032
+ actorId: ctx.actor.id,
6033
+ ...ctx.actor.tenantRef !== void 0 ? {
6034
+ tenantRef: ctx.actor.tenantRef
6035
+ } : {},
6036
+ args
6037
+ };
6038
+ const confirming = fields.confirm === true;
6039
+ const token = fields.confirmToken;
6040
+ if (confirming && !verifyConfirmToken(token, subject, {
6041
+ secret: secretNow()
6042
+ })) {
6043
+ throw new ConfirmTokenError("invalid", text.invalid);
6044
+ }
6045
+ const prepared = await steps.prepare(args, ctx);
6046
+ if (!confirming || token === void 0) {
6047
+ const preview = await steps.preview(prepared, ctx);
6048
+ const expiresAt = Date.now() + ttlMs;
6049
+ return {
6050
+ status: "preview",
6051
+ summary: preview.summary,
6052
+ ...preview.data !== void 0 ? {
6053
+ data: preview.data
6054
+ } : {},
6055
+ confirmToken: signConfirmToken(subject, {
6056
+ secret: secretNow(),
6057
+ expiresAt
6058
+ }),
6059
+ expiresAt: new Date(expiresAt).toISOString(),
6060
+ confirm: howToConfirm
6061
+ };
6062
+ }
6063
+ const hash = hashConfirmToken(token);
6064
+ if (store !== void 0) {
6065
+ const claimed = await store.claim({
6066
+ hash,
6067
+ actorRef: ctx.actor.id,
6068
+ tool: options.name,
6069
+ expiresAt: confirmTokenExpiry(token) ?? Date.now() + ttlMs
6070
+ });
6071
+ if (!claimed) {
6072
+ throw new ConfirmTokenError("used", text.used);
6073
+ }
6074
+ }
6075
+ try {
6076
+ const done = await steps.commit(prepared, ctx);
6077
+ return {
6078
+ status: "done",
6079
+ summary: done.summary,
6080
+ ...done.data !== void 0 ? {
6081
+ data: done.data
6082
+ } : {}
6083
+ };
6084
+ } catch (error) {
6085
+ await store?.release(hash).catch(() => void 0);
6086
+ throw error;
6087
+ }
6088
+ }
6089
+ };
6090
+ return {
6091
+ spec: {
6092
+ ...specFields,
6093
+ kind: "read",
6094
+ inputSchema: withConfirmFields(input)
6095
+ },
6096
+ handler
6097
+ };
6098
+ }
6099
+ __name(defineConfirmedTool, "defineConfirmedTool");
5765
6100
  export {
5766
6101
  AGENT_ACTOR_DIRECTORY,
5767
6102
  AGENT_ACTOR_RESOLVER,
5768
6103
  AGENT_APPROVAL_PORT,
5769
6104
  AGENT_ATTACHMENT_STAGING,
6105
+ AGENT_CONFIRM_TOKEN_STORE,
5770
6106
  AGENT_DEPS_FACTORY,
5771
6107
  AGENT_DIAGNOSTIC_EVENTS,
5772
6108
  AGENT_DURABLE_RUNNER,
@@ -5796,6 +6132,10 @@ export {
5796
6132
  ASK_TOOL_NAME,
5797
6133
  AgentRegistry,
5798
6134
  AgentStreamError,
6135
+ CONFIRM_JSON_SCHEMA_PROPERTIES,
6136
+ ClosedRolesPolicy,
6137
+ ConfirmTokenError,
6138
+ DEFAULT_CONFIRM_TTL_MS,
5799
6139
  DEFAULT_HISTORY_SUMMARY_INSTRUCTION,
5800
6140
  DEFAULT_INCREMENTAL_LOOKBACK_CHARS,
5801
6141
  DEFAULT_INTAKE_PREAMBLE,
@@ -5811,6 +6151,7 @@ export {
5811
6151
  ELICITATION_INPUT_TYPES,
5812
6152
  GLOBAL_SCOPE,
5813
6153
  InMemoryAgentStore,
6154
+ InMemoryConfirmTokenStore,
5814
6155
  MAX_ASK_QUESTIONS,
5815
6156
  OutputRejectedError,
5816
6157
  ProcessorFailedError,
@@ -5824,6 +6165,7 @@ export {
5824
6165
  RUN_NOT_ACTIVE_MESSAGE,
5825
6166
  RUN_NO_LONGER_RUNNING,
5826
6167
  RunCancelledError,
6168
+ SCHEMA_EXTENSION,
5827
6169
  SKILL_TOOL_DESCRIPTION,
5828
6170
  SKILL_TOOL_NAME,
5829
6171
  StructuredOutputError,
@@ -5846,7 +6188,10 @@ export {
5846
6188
  buildMemoryBlock,
5847
6189
  buildSkillsBlock,
5848
6190
  canActorUseTool,
6191
+ canonicalJson,
6192
+ closeEmptyRoles,
5849
6193
  compositeSkillProvider,
6194
+ confirmTokenExpiry,
5850
6195
  createFrameBuffer,
5851
6196
  createIncrementalGate,
5852
6197
  createNoopEmitUi,
@@ -5856,6 +6201,7 @@ export {
5856
6201
  decodeStreamEvent,
5857
6202
  defaultCanDecide,
5858
6203
  defaultScopeResolver,
6204
+ defineConfirmedTool,
5859
6205
  detachedDelivered,
5860
6206
  detachedStarted,
5861
6207
  detachedUnsettled,
@@ -5872,6 +6218,7 @@ export {
5872
6218
  findCatalogModel,
5873
6219
  gateFollowUps,
5874
6220
  gateTail,
6221
+ hashConfirmToken,
5875
6222
  invokeWithTransientRetry,
5876
6223
  isChatQueueStore,
5877
6224
  isControlFlowSignal,
@@ -5924,12 +6271,14 @@ export {
5924
6271
  runAgentLoop,
5925
6272
  runInputProcessors,
5926
6273
  runOutputProcessors,
6274
+ schemaExtensionOf,
5927
6275
  seedModelPrices,
5928
6276
  settleAll,
5929
6277
  settleDanglingToolCalls,
5930
6278
  settleDeadRun,
5931
6279
  settleElicitation,
5932
6280
  settleUnsettledDelegation,
6281
+ signConfirmToken,
5933
6282
  skillInputSchema,
5934
6283
  skillToolDefinition,
5935
6284
  skillWriteVerdict,
@@ -5948,8 +6297,10 @@ export {
5948
6297
  validateElicitationAnswer,
5949
6298
  validateElicitationValue,
5950
6299
  validateStructured,
6300
+ verifyConfirmToken,
5951
6301
  windowHistory,
5952
6302
  withAskTool,
6303
+ withConfirmFields,
5953
6304
  withMemoryTool,
5954
6305
  withSelectedModel,
5955
6306
  withSkillTool,