@eduwork/dsh-mail 0.1.2 → 0.2.1

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/lib/index.js CHANGED
@@ -78,6 +78,43 @@ function pick(source, keys, forced) {
78
78
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
79
79
  return result;
80
80
  }
81
+ /** Shared config references used by schema validators and plugin runtimes. */
82
+ const write = Symbol.for("cosmokit.volatile.write");
83
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
84
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
85
+ if (value === null || typeof value !== "object") return value;
86
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
87
+ ancestors.add(value);
88
+ try {
89
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
90
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
91
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
92
+ } finally {
93
+ ancestors.delete(value);
94
+ }
95
+ }
96
+ /**
97
+ * Create a detached reference containing an immutable copy of the supplied data.
98
+ * @param value - validated config data; class instances and functions are unsupported.
99
+ * @returns a reference whose value is updated only by its owning runtime.
100
+ */
101
+ function createVolatile(value) {
102
+ let current = snapshot(value);
103
+ return Object.freeze({
104
+ get: () => current,
105
+ [write]: (value) => {
106
+ current = value;
107
+ }
108
+ });
109
+ }
110
+ /**
111
+ * Identify references across ESM/CJS copies of the shared library.
112
+ * @param value - a parsed config value.
113
+ * @returns whether the value implements the shared reference protocol.
114
+ */
115
+ function isVolatile(value) {
116
+ return typeof value === "object" && value !== null && write in value;
117
+ }
81
118
  /** Test values using `instanceof` with a `toStringTag` fallback. */
82
119
  function is(type, value) {
83
120
  if (arguments.length === 1) return (value) => is(type, value);
@@ -158,26 +195,47 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
158
195
  }
159
196
  return result;
160
197
  }
161
- /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
198
+ /**
199
+ * Compare values recursively, treating two volatile references as equal regardless of value.
200
+ * Strict comparison distinguishes null/undefined, treats opaque objects by identity,
201
+ * compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
202
+ * @param a - first value.
203
+ * @param b - second value.
204
+ * @param strict - whether to require strict data equality outside volatile references.
205
+ * @returns whether the values compare equal.
206
+ */
162
207
  function deepEqual(a, b, strict) {
163
- if (a === b) return true;
164
- if (!strict && isNullable(a) && isNullable(b)) return true;
165
- if (typeof a !== typeof b) return false;
166
- if (typeof a !== "object") return false;
167
- if (!a || !b) return false;
168
- function check(test, then) {
169
- return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
170
- }
171
- return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
172
- if (a.byteLength !== b.byteLength) return false;
173
- const viewA = new Uint8Array(a);
174
- const viewB = new Uint8Array(b);
175
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
176
- return true;
177
- }) ?? Object.keys({
178
- ...a,
179
- ...b
180
- }).every((key) => deepEqual(a[key], b[key], strict));
208
+ const ancestors = /* @__PURE__ */ new Set();
209
+ function compare(a, b) {
210
+ if (a === b) return true;
211
+ if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
212
+ if (!strict && isNullable(a) && isNullable(b)) return true;
213
+ if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
214
+ if (ancestors.has(a)) return false;
215
+ function check(test, then) {
216
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
217
+ }
218
+ ancestors.add(a);
219
+ try {
220
+ return check(Array.isArray, (a, b) => {
221
+ if (a.length !== b.length) return false;
222
+ for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
223
+ return true;
224
+ }) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
225
+ if (a.byteLength !== b.byteLength) return false;
226
+ const viewA = new Uint8Array(a);
227
+ const viewB = new Uint8Array(b);
228
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
229
+ return true;
230
+ }) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
231
+ ...a,
232
+ ...b
233
+ }).every((key) => compare(a[key], b[key])));
234
+ } finally {
235
+ ancestors.delete(a);
236
+ }
237
+ }
238
+ return compare(a, b);
181
239
  }
182
240
  /** Time constants plus parsing and formatting helpers. */
183
241
  var Time;
@@ -426,6 +484,7 @@ Schema.prototype.pattern = function pattern(regexp) {
426
484
  return schema;
427
485
  };
428
486
  Schema.prototype.simplify = function simplify(value) {
487
+ if (isVolatile(value)) value = value.get();
429
488
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
430
489
  if (isNullable(value)) return value;
431
490
  if (this.type === "object" || this.type === "dict") {
@@ -482,12 +541,49 @@ for (const key of [
482
541
  };
483
542
  return schema;
484
543
  } });
544
+ Schema.prototype.volatile = function volatile() {
545
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
546
+ return this.extra("volatile", true);
547
+ };
485
548
  const resolvers = {};
549
+ const checkedVolatile = Symbol("checked-volatile-schema");
550
+ function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
551
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
552
+ if (states.has(blocked)) return;
553
+ states.add(blocked);
554
+ seen.set(schema, states);
555
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
556
+ const nested = blocked || !!schema.meta?.volatile;
557
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
558
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
559
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
560
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
561
+ }
486
562
  Schema.extend = function extend(type, resolve) {
487
563
  resolvers[type] = resolve;
488
564
  };
489
565
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
490
566
  if (!schema) return [data];
567
+ if (!options[checkedVolatile]) {
568
+ validateVolatileSchema(schema, options.path);
569
+ options = {
570
+ ...options,
571
+ [checkedVolatile]: true
572
+ };
573
+ }
574
+ if (schema.meta?.volatile) {
575
+ const inner = Schema(schema);
576
+ inner.meta = {
577
+ ...schema.meta,
578
+ volatile: false
579
+ };
580
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
581
+ try {
582
+ return [createVolatile(value), adapted];
583
+ } catch (error) {
584
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
585
+ }
586
+ }
491
587
  if (options.ignore?.(data, schema)) return [data];
492
588
  if (isNullable(data) && schema.type !== "lazy") {
493
589
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -590,6 +686,7 @@ Schema.extend("lazy", (data, schema, options, strict) => {
590
686
  ...schema.meta,
591
687
  ...schema.inner.meta
592
688
  };
689
+ validateVolatileSchema(schema.inner, options.path, true);
593
690
  }
594
691
  return Schema.resolve(data, schema.inner, options, strict);
595
692
  });
@@ -689,7 +786,7 @@ function property(data, key, schema, options) {
689
786
  } catch (e) {
690
787
  if (!options?.autofix) throw e;
691
788
  delete data[key];
692
- return schema.meta.default;
789
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
693
790
  }
694
791
  }
695
792
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -848,20 +945,21 @@ defineMethod("transform", [
848
945
  const MailSettingsSchema = Schema.object({
849
946
  readEnabled: Schema.boolean().default(DEFAULTS.readEnabled),
850
947
  sendEnabled: Schema.boolean().default(DEFAULTS.sendEnabled),
851
- email: Schema.string().default(DEFAULTS.email),
852
- username: Schema.string().default(DEFAULTS.username),
853
- fromName: Schema.string().default(DEFAULTS.fromName),
854
- inboxFolder: Schema.string().default(DEFAULTS.inboxFolder),
855
- imapHost: Schema.string().default(DEFAULTS.imapHost),
856
- imapPort: Schema.number().default(DEFAULTS.imapPort),
948
+ email: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.email),
949
+ username: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.username),
950
+ fromName: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.fromName),
951
+ inboxFolder: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.inboxFolder),
952
+ imapHost: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.imapHost),
953
+ imapPort: Schema.number().step(1).min(1).max(65535).default(DEFAULTS.imapPort),
857
954
  imapTls: Schema.union(["implicit", "starttls"]).default(DEFAULTS.imapTls),
858
- smtpHost: Schema.string().default(DEFAULTS.smtpHost),
859
- smtpPort: Schema.number().default(DEFAULTS.smtpPort),
955
+ smtpHost: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.smtpHost),
956
+ smtpPort: Schema.number().step(1).min(1).max(65535).default(DEFAULTS.smtpPort),
860
957
  smtpTls: Schema.union(["implicit", "starttls"]).default(DEFAULTS.smtpTls),
861
- maxBodyChars: Schema.number().default(DEFAULTS.maxBodyChars),
862
- maxMessageBytes: Schema.number().default(DEFAULTS.maxMessageBytes),
863
- maxAttachmentBytes: Schema.number().default(DEFAULTS.maxAttachmentBytes)
958
+ maxBodyChars: Schema.number().step(1).min(1e3).max(2e5).default(DEFAULTS.maxBodyChars),
959
+ maxMessageBytes: Schema.number().step(1).min(1048576).max(104857600).default(DEFAULTS.maxMessageBytes),
960
+ maxAttachmentBytes: Schema.number().step(1).min(1024).max(104857600).default(DEFAULTS.maxAttachmentBytes)
864
961
  });
962
+ const Config = typeof Schema.string().volatile === "function" ? Schema.object(Object.fromEntries(Object.entries(MailSettingsSchema.dict).map(([key, field]) => [key, field.volatile()]))) : void 0;
865
963
  function settingsBase(config = {}) {
866
964
  const base = { ...DEFAULTS };
867
965
  for (const key of Object.keys(DEFAULTS)) if (config[key] !== void 0) base[key] = config[key];
@@ -910,6 +1008,7 @@ const SEND_APPROVAL_REASON = "Send this email through the configured SMTP accoun
910
1008
  * after the user has explicitly enabled sending in Mail assistant settings.
911
1009
  */
912
1010
  function decideMailPermission(ctx, exec, next) {
1011
+ if (exec.name === TOOL_NAMES.attachment) return decideAttachmentWrite(ctx, exec, next);
913
1012
  if (exec.name !== TOOL_NAMES.send) return next();
914
1013
  const agent = exec.agent;
915
1014
  if (agent === void 0) return Promise.resolve({
@@ -919,9 +1018,50 @@ function decideMailPermission(ctx, exec, next) {
919
1018
  if (ctx.permissionPresets.current(agent.session) === "danger-full-access") return next();
920
1019
  return Promise.resolve({
921
1020
  kind: "ask",
922
- reason: SEND_APPROVAL_REASON
1021
+ reason: SEND_APPROVAL_REASON,
1022
+ displayReason: {
1023
+ en: SEND_APPROVAL_REASON,
1024
+ zh: "允许通过已配置的 SMTP 账号发送这封邮件。请核对工具调用中显示的收件人、主题和附件。"
1025
+ }
923
1026
  });
924
1027
  }
1028
+ async function decideAttachmentWrite(ctx, exec, next) {
1029
+ if (!exec.agent) return {
1030
+ kind: "deny",
1031
+ reason: "Attachment downloads require an Agent-backed session"
1032
+ };
1033
+ if (!exec.agent.session.header?.cwd) return {
1034
+ kind: "deny",
1035
+ reason: "Attachment downloads require a session workspace"
1036
+ };
1037
+ const resolver = ctx.get?.("sandboxPolicy");
1038
+ const policy = resolver ? resolver.resolve({ session: exec.agent.session }) : { mode: ctx.permissionPresets.current(exec.agent.session) };
1039
+ if (policy?.mode === "danger-full-access") return next();
1040
+ if (policy?.mode === "workspace-write") {
1041
+ if (policy.workspaceRoot !== void 0) {
1042
+ const options = {
1043
+ cwd: exec.agent.session.header.cwd,
1044
+ signal: exec.signal
1045
+ };
1046
+ const root = await ctx.fs.resolve(".", options);
1047
+ const allowed = await ctx.fs.resolve(policy.workspaceRoot, options);
1048
+ if (!ctx.fs.contains(allowed, root)) return {
1049
+ kind: "deny",
1050
+ reason: "Attachment workspace is outside the allowed write root"
1051
+ };
1052
+ }
1053
+ return next();
1054
+ }
1055
+ const reason = "Save this mail attachment in the current workspace";
1056
+ return {
1057
+ kind: "ask",
1058
+ reason,
1059
+ displayReason: {
1060
+ en: reason,
1061
+ zh: "允许将这封邮件的附件保存到当前工作区"
1062
+ }
1063
+ };
1064
+ }
925
1065
  Object.freeze({ SEND_APPROVAL_REASON });
926
1066
  //#endregion
927
1067
  //#region node_modules/pino-std-serializers/lib/err-helpers.js
@@ -101893,11 +102033,12 @@ async function readWorkspaceAttachments(ctx, paths, exec, maxBytes) {
101893
102033
  if (!Array.isArray(paths) || paths.length > 10 || paths.some((path) => typeof path !== "string" || path.trim() === "")) throw new Error("attachments must be an array of at most 10 non-empty workspace paths");
101894
102034
  const workspace = workspaceOf(exec);
101895
102035
  const workspaceTarget = await ctx.fs.resolve(workspace, { signal: exec.signal });
102036
+ const cwd = ctx.get?.("workingDirectory")?.get(exec.agent.session) ?? workspace;
101896
102037
  const attachments = [];
101897
102038
  let total = 0;
101898
102039
  for (const raw of paths) {
101899
102040
  const target = await ctx.fs.resolve(raw.trim(), {
101900
- cwd: workspace,
102041
+ cwd,
101901
102042
  signal: exec.signal
101902
102043
  });
101903
102044
  if (!ctx.fs.contains(workspaceTarget, target)) throw new Error(`Attachment is outside the session workspace: ${raw}`);
@@ -102412,14 +102553,16 @@ const inject = [
102412
102553
  "permissionPresets"
102413
102554
  ];
102414
102555
  function apply(ctx, config = {}) {
102415
- const base = settingsBase(config);
102556
+ const native = typeof ctx.settings.register !== "function";
102557
+ const readLive = () => Object.fromEntries(Object.keys(DEFAULTS).map((key) => [key, config[key].get()]));
102558
+ const base = native ? readLive() : settingsBase(config);
102416
102559
  validateSettings(base);
102417
- const scope = ctx.settings.register(SETTINGS_NAMESPACE, MailSettingsSchema, {
102560
+ const scope = native ? { get: readLive } : ctx.settings.register(SETTINGS_NAMESPACE, MailSettingsSchema, {
102418
102561
  base,
102419
102562
  applies: "live",
102420
102563
  validate: validateSettings
102421
102564
  });
102422
- const getSettings = () => currentSettings(ctx, scope, config);
102565
+ const getSettings = () => currentSettings(ctx, scope, native ? {} : config);
102423
102566
  ctx.on("tools/pre-execute", (exec, next) => decideMailPermission(ctx, exec, next));
102424
102567
  installTools(ctx, getSettings);
102425
102568
  ctx.tools.guard((exec) => {
@@ -102453,9 +102596,10 @@ function apply(ctx, config = {}) {
102453
102596
  var host_default = {
102454
102597
  name,
102455
102598
  inject,
102599
+ Config,
102456
102600
  apply
102457
102601
  };
102458
102602
  //#endregion
102459
- export { DEFAULTS, MailSettingsSchema, PASSWORD_REF_NAME, SETTINGS_NAMESPACE, TOOL_NAMES, apply, host_default as default, inject, name, validateSettings };
102603
+ export { Config, DEFAULTS, MailSettingsSchema, PASSWORD_REF_NAME, SETTINGS_NAMESPACE, TOOL_NAMES, apply, host_default as default, inject, name, validateSettings };
102460
102604
 
102461
102605
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduwork/dsh-mail",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Standards-based IMAP/SMTP mail assistant for DeepSeek Harness with explicit read/send controls and approval-aware delivery",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -50,7 +50,8 @@
50
50
  "inject": [
51
51
  "@deepseek-ai/dsh-api-remotes",
52
52
  "@deepseek-ai/dsh-client-ui-renderer",
53
- "@deepseek-ai/dsh-client-ui-settings"
53
+ "@deepseek-ai/dsh-client-ui-settings",
54
+ "@deepseek-ai/dsh-client-locale"
54
55
  ],
55
56
  "platform": "web",
56
57
  "immediately": true
@@ -78,7 +79,7 @@
78
79
  "prepare": "npm run build"
79
80
  },
80
81
  "engines": {
81
- "node": ">=22.0.0"
82
+ "node": ">=24.18.0"
82
83
  },
83
84
  "license": "MIT",
84
85
  "publishConfig": {
@@ -86,22 +87,23 @@
86
87
  },
87
88
  "repository": {
88
89
  "type": "git",
89
- "url": "git+https://github.com/ecnu/EduWork.git",
90
+ "url": "git+https://github.com/ECNU/EduWork.git",
90
91
  "directory": "packages/dsh-mail"
91
92
  },
92
93
  "bugs": {
93
- "url": "https://github.com/ecnu/EduWork/issues"
94
+ "url": "https://github.com/ECNU/EduWork/issues"
94
95
  },
95
- "homepage": "https://github.com/ecnu/EduWork/tree/main/packages/dsh-mail#readme",
96
+ "homepage": "https://github.com/ECNU/EduWork/tree/main/packages/dsh-mail#readme",
96
97
  "peerDependencies": {
97
- "@deepseek-ai/cordis": "4.0.2",
98
- "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
99
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
100
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
101
- "@deepseek-ai/dsh-credentials": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
102
- "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
103
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
104
- "react": "^18.2.0"
98
+ "@deepseek-ai/cordis": ">=4.0.5 <4.1.0",
99
+ "@deepseek-ai/dsh-api-remotes": ">=0.2.1-rc.2 <0.3.0-0",
100
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.2 <0.3.0-0",
101
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.1-rc.2 <0.3.0-0",
102
+ "@deepseek-ai/dsh-credentials": ">=0.2.1-rc.2 <0.3.0-0",
103
+ "@deepseek-ai/dsh-permission-presets": ">=0.2.1-rc.2 <0.3.0-0",
104
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.2 <0.3.0-0",
105
+ "react": "^18.2.0",
106
+ "@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.2 <0.3.0-0"
105
107
  },
106
108
  "peerDependenciesMeta": {
107
109
  "@deepseek-ai/cordis": {
@@ -127,10 +129,13 @@
127
129
  },
128
130
  "react": {
129
131
  "optional": true
132
+ },
133
+ "@deepseek-ai/dsh-client-locale": {
134
+ "optional": true
130
135
  }
131
136
  },
132
137
  "devDependencies": {
133
- "@deepseek-ai/schemastery": "3.18.2",
138
+ "@deepseek-ai/schemastery": "3.18.5",
134
139
  "@types/node": "^26.0.0",
135
140
  "@types/react": "^18.3.28",
136
141
  "imapflow": "^1.7.0",
@@ -138,7 +143,10 @@
138
143
  "nodemailer": "^9.1.0",
139
144
  "react": "^18.3.1",
140
145
  "tsdown": "0.22.14",
141
- "typescript": "^5.9.3"
146
+ "typescript": "^5.9.3",
147
+ "@deepseek-ai/dsh": "0.2.1-rc.2",
148
+ "@deepseek-ai/cordis": "4.0.5",
149
+ "semver": "7.7.4"
142
150
  },
143
151
  "directories": {
144
152
  "doc": "docs",