1
0
Эх сурвалжийг харах

feat(util): mint UUIDs without crypto.randomUUID in every context

imccyu 2 долоо хоног өмнө
parent
commit
0bee546177
33 өөрчлөгдсөн 362 нэмэгдсэн , 31 устгасан
  1. 8 0
      .oxlintrc.json
  2. 1 1
      packages/client/tsdown.client.ts
  3. 14 12
      packages/client/ui-conversation/package.json
  4. 2 1
      packages/client/ui-conversation/src/client/service.ts
  5. 3 0
      packages/client/ui-conversation/tsconfig.json
  6. 1 0
      packages/experimental/webworker-runtime/package.json
  7. 5 2
      packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts
  8. 2 1
      packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts
  9. 19 0
      packages/experimental/webworker-runtime/src/node/globals/crypto.ts
  10. 2 0
      packages/experimental/webworker-runtime/src/worker.ts
  11. 36 0
      packages/experimental/webworker-runtime/tests/node/crypto-globals.spec.ts
  12. 3 0
      packages/experimental/webworker-runtime/tsconfig.json
  13. 4 4
      packages/host/apiproxy/package.json
  14. 4 2
      packages/host/apiproxy/src/fetch/client.ts
  15. 3 0
      packages/host/apiproxy/tsconfig.json
  16. 1 0
      packages/interaction/commands/package.json
  17. 2 1
      packages/interaction/commands/src/index.ts
  18. 3 0
      packages/interaction/commands/tsconfig.json
  19. 1 0
      packages/llm/llm/package.json
  20. 2 1
      packages/llm/llm/src/message.ts
  21. 3 0
      packages/llm/llm/tsconfig.json
  22. 6 0
      packages/util/crypto/README.i18n.yaml
  23. 31 0
      packages/util/crypto/README.md
  24. 31 0
      packages/util/crypto/README.zh.md
  25. 42 0
      packages/util/crypto/package.json
  26. 26 0
      packages/util/crypto/src/index.ts
  27. 30 0
      packages/util/crypto/src/invariant.ts
  28. 29 0
      packages/util/crypto/tests/uuid.spec.ts
  29. 15 0
      packages/util/crypto/tsconfig.json
  30. 24 0
      pnpm-lock.yaml
  31. 6 6
      scripts/lint-rule-fingerprint.spec.ts
  32. 2 0
      tsconfig.base.json
  33. 1 0
      tsconfig.host.json

+ 8 - 0
.oxlintrc.json

@@ -57,6 +57,14 @@
           }
         ],
         "no-useless-constructor": "error",
+        "no-restricted-properties": [
+          "error",
+          {
+            "object": "crypto",
+            "property": "randomUUID",
+            "message": "browsers withhold crypto.randomUUID outside secure contexts (plain-HTTP LAN pages); mint through @deepseek-ai/dsh-util-crypto instead"
+          }
+        ],
         "typescript/await-thenable": "error",
         "typescript/ban-ts-comment": [
           "error",

+ 1 - 1
packages/client/tsdown.client.ts

@@ -58,7 +58,7 @@ function styleInjectionModule(
  * Everything else under @deepseek-ai/* is either a module-table entry
  * (external) or a leak the purity gate rejects.
  */
-export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand)(\/|$)/
+export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand|util-crypto)(\/|$)/
 
 /**
  * Vendored framework libraries: rescoped into @deepseek-ai, so the gate below

+ 14 - 12
packages/client/ui-conversation/package.json

@@ -48,8 +48,8 @@
   },
   "license": "MIT",
   "dependencies": {
-    "clsx": "^2.0.0",
-    "@deepseek-ai/schemastery": "workspace:^"
+    "@deepseek-ai/schemastery": "workspace:^",
+    "clsx": "^2.0.0"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -60,21 +60,22 @@
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-compaction": "workspace:^",
+    "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
+    "@deepseek-ai/dsh-permission-presets": "workspace:^",
+    "@deepseek-ai/dsh-plan-mode": "workspace:^",
     "@deepseek-ai/dsh-session-stats": "workspace:^",
+    "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
+    "@deepseek-ai/dsh-tool-todo": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
-    "@deepseek-ai/dsh-settings": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
-    "@deepseek-ai/dsh-goal": "workspace:^",
-    "@deepseek-ai/dsh-permission-presets": "workspace:^",
-    "@deepseek-ai/dsh-plan-mode": "workspace:^",
-    "@deepseek-ai/dsh-tool-todo": "workspace:^"
+    "@deepseek-ai/dsh-util-crypto": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -86,10 +87,10 @@
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-compaction": "workspace:^",
@@ -100,12 +101,13 @@
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-stats": "workspace:^",
+    "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
     "@deepseek-ai/dsh-tool-todo": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
+    "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "@types/react": "~18.3.1",
-    "react": "^18.2.0",
-    "@deepseek-ai/dsh-settings": "workspace:^"
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 2 - 1
packages/client/ui-conversation/src/client/service.ts

@@ -9,6 +9,7 @@
  */
 import { Service } from '@deepseek-ai/cordis'
 import type { Context } from '@deepseek-ai/cordis'
+import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
 // Type-only imports: a plugin-to-plugin value import is a bundle purity
 // error, so scope resolution goes through the sessions service (scopeOf
 // method) instead of the standalone helper.
@@ -63,7 +64,7 @@ export interface IConversation {
 function browserDraftAttachment(file: File): ComposerAttachment {
   return {
     kind: 'image',
-    id: crypto.randomUUID() as DraftAttachmentId,
+    id: randomUUID() as DraftAttachmentId,
     previewUrl: URL.createObjectURL(file),
     file,
   }

+ 3 - 0
packages/client/ui-conversation/tsconfig.json

@@ -76,6 +76,9 @@
     },
     {
       "path": "../ui-settings"
+    },
+    {
+      "path": "../../util/crypto"
     }
   ],
   "exclude": [

+ 1 - 0
packages/experimental/webworker-runtime/package.json

@@ -30,6 +30,7 @@
   },
   "license": "MIT",
   "dependencies": {
+    "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "@noble/hashes": "^2.3.0",
     "@yarnpkg/parsers": "^3.1.0",
     "acorn": "^8.17.0",

+ 5 - 2
packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts

@@ -5,6 +5,7 @@
  */
 import { sha1 } from '@noble/hashes/legacy.js'
 import { sha256, sha512 } from '@noble/hashes/sha2.js'
+import { randomUUID as mintUUID } from '@deepseek-ai/dsh-util-crypto'
 import { Buffer } from 'buffer'
 
 type Hasher = (input: Uint8Array) => Uint8Array
@@ -73,11 +74,13 @@ export function randomBytes(size: number): Buffer<ArrayBuffer> {
 }
 
 /**
- * Random v4 UUID.
+ * Random v4 UUID. Delegated to the repository's own mint rather than to
+ * `crypto.randomUUID`, which browsers expose only in secure contexts — a
+ * preview served over plain HTTP on a LAN address has no `randomUUID`.
  * @returns the UUID string.
  */
 export function randomUUID(): import('node:crypto').UUID {
-  return globalThis.crypto.randomUUID()
+  return mintUUID()
 }
 
 /**

+ 2 - 1
packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts

@@ -200,7 +200,8 @@ export function mkdirSync(path: PathArg, options?: { recursive?: boolean }): str
  * @returns the created directory path.
  */
 export function mkdtempSync(prefix: string): string {
-  const suffix = globalThis.crypto.randomUUID().replaceAll('-', '').slice(0, 6)
+  // Not crypto.randomUUID: browsers expose that only in secure contexts.
+  const suffix = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(3)), byte => byte.toString(16).padStart(2, '0')).join('')
   const target = `${prefix}${suffix}`
   vfs().mkdirSync(target, { recursive: true })
   return target

+ 19 - 0
packages/experimental/webworker-runtime/src/node/globals/crypto.ts

@@ -0,0 +1,19 @@
+/**
+ * Fill the `crypto.randomUUID` gap on insecure origins. Browsers expose
+ * `randomUUID` only in secure contexts, and a preview served over plain HTTP
+ * on a LAN address is not one — while product code (bundled and VFS-loaded
+ * alike) reaches the global directly, Node-style. The worker patches the one
+ * `crypto` instance instead of teaching every caller.
+ */
+import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
+
+/** Install `crypto.randomUUID` when the context withholds it. */
+export function installCryptoGlobals(): void {
+  // In a secure context the platform method is present and stays untouched.
+  if (typeof globalThis.crypto.randomUUID === 'function') return
+  Object.defineProperty(globalThis.crypto, 'randomUUID', {
+    value: randomUUID,
+    configurable: true,
+    writable: true,
+  })
+}

+ 2 - 0
packages/experimental/webworker-runtime/src/worker.ts

@@ -21,12 +21,14 @@ import { createNodeBuiltins, REPLACED_PREFIXES } from './node/builtins.ts'
 import { whenRequestListener } from './node/builtin_modules/implemented/http.ts'
 import { installTimerGlobals } from './node/globals/timers.ts'
 import { installProcessGlobal } from './node/globals/process.ts'
+import { installCryptoGlobals } from './node/globals/crypto.ts'
 import { isShellStartFrame } from './shell/process/protocol.ts'
 import { runShellProcess } from './shell/process/host.ts'
 
 // Before the timer globals, so the wrappers close over the patched platform.
 installAsyncContextHooks()
 installTimerGlobals()
+installCryptoGlobals()
 
 let host: { handleMessage(data: unknown): void } | undefined
 let shellRole = false

+ 36 - 0
packages/experimental/webworker-runtime/tests/node/crypto-globals.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * The crypto global patch and the shim UUID it installs: on an insecure
+ * origin the platform withholds `crypto.randomUUID` while product code calls
+ * it off the global, so the worker fills the one missing method — and leaves
+ * a platform that already has it untouched.
+ */
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { installCryptoGlobals } from '../../src/node/globals/crypto.ts'
+import { randomUUID } from '../../src/node/builtin_modules/implemented/crypto.ts'
+
+const V4_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
+
+describe('the shim randomUUID', () => {
+  it('emits RFC 9562 v4 ids without touching the platform method', () => {
+    for (let round = 0; round < 32; round += 1) expect(randomUUID()).toMatch(V4_SHAPE)
+    expect(new Set(Array.from({ length: 32 }, () => randomUUID())).size).toBe(32)
+  })
+})
+
+describe('installCryptoGlobals', () => {
+  afterEach(() => { vi.unstubAllGlobals() })
+
+  it('fills randomUUID on a crypto that lacks it, the insecure-origin shape', () => {
+    const bare = { getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto) }
+    vi.stubGlobal('crypto', bare)
+    installCryptoGlobals()
+    expect((globalThis.crypto as Crypto).randomUUID()).toMatch(V4_SHAPE)
+  })
+
+  it('leaves a platform that already provides randomUUID untouched', () => {
+    const platform = (): string => 'platform-owned'
+    vi.stubGlobal('crypto', { randomUUID: platform })
+    installCryptoGlobals()
+    expect(globalThis.crypto.randomUUID).toBe(platform)
+  })
+})

+ 3 - 0
packages/experimental/webworker-runtime/tsconfig.json

@@ -31,6 +31,9 @@
     },
     {
       "path": "../../runtime-diagnostics/invariants"
+    },
+    {
+      "path": "../../util/crypto"
     }
   ]
 }

+ 4 - 4
packages/host/apiproxy/package.json

@@ -45,15 +45,16 @@
   ],
   "license": "MIT",
   "dependencies": {
-    "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-default-model": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
+    "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-brand": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
+    "@deepseek-ai/dsh-jobs": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-native-command": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
@@ -65,10 +66,10 @@
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-skill": "workspace:^",
     "@deepseek-ai/dsh-subagent": "workspace:^",
-    "@deepseek-ai/dsh-jobs": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/dsh-user-questions": "workspace:^",
+    "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "@deepseek-ai/dsh-workspace": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^",
     "fflate": "^0.8.2",
@@ -88,7 +89,6 @@
     "@deepseek-ai/dsh-storage": "workspace:^",
     "@deepseek-ai/dsh-storage-domain": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
-    "@deepseek-ai/dsh-typert-registry": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-typert-registry": "workspace:^"
   }
 }

+ 4 - 2
packages/host/apiproxy/src/fetch/client.ts

@@ -6,6 +6,7 @@
  */
 
 import type { z } from 'zod'
+import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
 import type { ApiProxy, HostFrame, MuxFrame } from '../api/index.ts'
 import type { RequestPayload, ResponseValue, RpcMethodMap } from '../api/rpc-map.ts'
 import type { ClientRequest, ClientResponse, RpcMessage, RpcReceipt, RpcRequest, RpcResponse, ServerRequest } from '../api/rpc.ts'
@@ -296,8 +297,9 @@ export abstract class AbstractApiClient implements IApiClient {
   }
 
   protected mintRpcId(): RpcId {
-    // crypto.randomUUID is a Web API (browser + Node ≥19): keeps this base platform-neutral.
-    return RpcId(crypto.randomUUID())
+    // Not crypto.randomUUID: browsers withhold it outside secure contexts,
+    // and this base also mints on pages served over plain HTTP.
+    return RpcId(randomUUID())
   }
 
   /**

+ 3 - 0
packages/host/apiproxy/tsconfig.json

@@ -100,6 +100,9 @@
     },
     {
       "path": "../../util/native-command"
+    },
+    {
+      "path": "../../util/crypto"
     }
   ]
 }

+ 1 - 0
packages/interaction/commands/package.json

@@ -64,6 +64,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "dependencies": {
+    "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "zod": "^4.4.3"
   },
   "devDependencies": {

+ 2 - 1
packages/interaction/commands/src/index.ts

@@ -4,6 +4,7 @@
  */
 
 import { Context } from '@deepseek-ai/cordis'
+import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment'
 import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types'
@@ -256,7 +257,7 @@ export class CommandRuntime extends TypertRemoteService {
   /** Monotonic per-instance counter behind {@link mintCommandId}. */
   private commandSeq = 0
   /** Instance token keeping minted ids unique across process restarts over one resumed log. */
-  private readonly instanceToken = crypto.randomUUID().slice(0, 8)
+  private readonly instanceToken = randomUUID().slice(0, 8)
 
   constructor(ctx: Context) {
     super(ctx, 'commands')

+ 3 - 0
packages/interaction/commands/tsconfig.json

@@ -37,6 +37,9 @@
     },
     {
       "path": "../../typert/protocol"
+    },
+    {
+      "path": "../../util/crypto"
     }
   ]
 }

+ 1 - 0
packages/llm/llm/package.json

@@ -52,6 +52,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "dependencies": {
+    "@deepseek-ai/dsh-util-crypto": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "devDependencies": {

+ 2 - 1
packages/llm/llm/src/message.ts

@@ -1,5 +1,6 @@
 /** Message value types, identity, and immutable construction helpers. */
 
+import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
 import { MessageId, type CallId } from './brand.ts'
 import { deepFreeze } from './call-config.ts'
 import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts'
@@ -180,7 +181,7 @@ export function createMessage<T extends NewMessage>(
 ): T & Pick<Message, 'id'> {
   return freezeMessage({
     ...input,
-    id: MessageId(crypto.randomUUID()),
+    id: MessageId(randomUUID()),
   })
 }
 

+ 3 - 0
packages/llm/llm/tsconfig.json

@@ -25,6 +25,9 @@
     },
     {
       "path": "../../util/timeout"
+    },
+    {
+      "path": "../../util/crypto"
     }
   ]
 }

+ 6 - 0
packages/util/crypto/README.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write packages/util/crypto/README.md
+README.md: 456f7b8dc9476b6e2dc4844964912410ec934c2d
+README.zh.md: 9b9836b4bef528b3aeae5531ceba549b1963d4bd

+ 31 - 0
packages/util/crypto/README.md

@@ -0,0 +1,31 @@
+# dsh-util-crypto
+
+English | [中文](README.zh.md)
+
+Zero-dependency v4 UUID minting over `crypto.getRandomValues` — the one random primitive every shipped context provides. `crypto.randomUUID` is a secure-context Web API: a page or worker served over plain HTTP on a LAN address (the browser preview deployment) has no such method, so code that must run there cannot call it. The repository-wide `no-restricted-properties` lint rule points `crypto.randomUUID` callers here; Node-only code importing `randomUUID` from `node:crypto` stays as it is.
+
+It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state.
+
+## API
+
+```ts
+import { randomUUID, type Uuid } from '@deepseek-ai/dsh-util-crypto'
+```
+
+| Export | Role |
+|---|---|
+| `randomUUID()` | Random RFC 9562 v4 UUID string, minted from `crypto.getRandomValues`. Drop-in for `crypto.randomUUID()`. |
+| `Uuid` | The five-group UUID string type, matching `crypto.randomUUID`'s declared return shape. |
+
+## Model Experience
+
+None directly; consumers mint request, session, and attachment identifiers with it, none of which enter prompts as semantic content.
+
+#### KV Cache effect
+
+No direct invalidation; identifier-minting consumers own any request changes.
+
+## Known Limitations and Deferred Work
+
+- **v4 only** — no other UUID versions, namespaces, or parsing; consumers needing more should take a real UUID dependency.
+- **Uniqueness is probabilistic** — 122 random bits, the same guarantee `crypto.randomUUID` gives; nothing here detects collisions.

+ 31 - 0
packages/util/crypto/README.zh.md

@@ -0,0 +1,31 @@
+# dsh-util-crypto
+
+[English](README.md) | 中文
+
+零依赖的 v4 UUID 铸造,基于 `crypto.getRandomValues`——所有发布上下文都提供的那个随机原语。`crypto.randomUUID` 是安全上下文限定的 Web API:经普通 HTTP 在局域网地址上提供的页面或 worker(浏览器预览部署)根本没有这个方法,必须在那里运行的代码不能调它。全仓 `no-restricted-properties` lint 规则把 `crypto.randomUUID` 的调用者指到这里;只跑在 Node 的代码从 `node:crypto` 导入 `randomUUID` 维持原样。
+
+它是**库,不是服务也不是插件**:无 `ctx`、不注册任何东西、不持有状态。
+
+## API
+
+```ts
+import { randomUUID, type Uuid } from '@deepseek-ai/dsh-util-crypto'
+```
+
+| 导出 | 角色 |
+|---|---|
+| `randomUUID()` | 随机 RFC 9562 v4 UUID 字符串,由 `crypto.getRandomValues` 铸造。可原位替换 `crypto.randomUUID()`。 |
+| `Uuid` | 五段式 UUID 字符串类型,与 `crypto.randomUUID` 声明的返回形状一致。 |
+
+## Model Experience
+
+无直接影响;消费方用它铸造请求、会话与附件标识符,均不作为语义内容进入提示词。
+
+#### KV Cache effect
+
+无直接失效;铸造标识符的消费方自行负责其请求变化。
+
+## Known Limitations and Deferred Work
+
+- **仅 v4**——不提供其他 UUID 版本、命名空间或解析;需要更多能力的消费方应引入真正的 UUID 依赖。
+- **唯一性是概率性的**——122 位随机,与 `crypto.randomUUID` 同级保证;此处不做碰撞检测。

+ 42 - 0
packages/util/crypto/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@deepseek-ai/dsh-util-crypto",
+  "description": "Zero-dependency crypto-adjacent helpers usable in every context (browser, worker, Node) including insecure origins: v4 UUID minting over crypto.getRandomValues today, room for siblings",
+  "version": "0.1.0-rc.8",
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+    "directory": "packages/util/crypto"
+  },
+  "type": "module",
+  "main": "lib/index.js",
+  "types": "lib/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./lib/types/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./invariant": {
+      "types": "./lib/types/invariant.d.ts",
+      "default": "./lib/invariant.js"
+    },
+    "./src/*": "./src/*",
+    "./package.json": "./package.json"
+  },
+  "files": [
+    "lib/index.js",
+    "lib/invariant.js",
+    "lib/types/**/*.d.ts"
+  ],
+  "license": "MIT",
+  "peerDependencies": {
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^"
+  },
+  "devDependencies": {
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^"
+  }
+}

+ 26 - 0
packages/util/crypto/src/index.ts

@@ -0,0 +1,26 @@
+/**
+ * UUID minting that works in every JavaScript context this repository ships
+ * to. `crypto.randomUUID` is a secure-context Web API — a page or worker
+ * served over plain HTTP on a LAN address has no such method — while
+ * `crypto.getRandomValues` is unrestricted everywhere (browsers, workers,
+ * Node ≥ 19). One implementation here replaces per-caller polyfills; the
+ * `no-restricted-properties` lint rule points `crypto.randomUUID` callers at
+ * this module.
+ * @module @deepseek-ai/dsh-util-crypto
+ */
+
+/** RFC 9562 UUID string, the shape `crypto.randomUUID` declares. */
+export type Uuid = `${string}-${string}-${string}-${string}-${string}`
+
+/**
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
+ * @returns the UUID string.
+ */
+export function randomUUID(): Uuid {
+  const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))
+  // RFC 9562 §5.4: version 4 in the high nibble of byte 6, variant 10 in byte 8.
+  bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40
+  bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
+  const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
+  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
+}

+ 30 - 0
packages/util/crypto/src/invariant.ts

@@ -0,0 +1,30 @@
+/**
+ * Package-owned invariant companion for `@deepseek-ai/dsh-util-crypto`.
+ * @module @deepseek-ai/dsh-util-crypto/invariant
+ */
+
+/* jscpd:ignore-start */
+import type { Context } from '@deepseek-ai/cordis'
+import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+
+const PACKAGE_NAME = '@deepseek-ai/dsh-util-crypto'
+
+/** Cordis companion plugin name. */
+export const name = 'util-crypto-invariant'
+/** Service required before the companion can reserve package ownership. */
+export const inject = ['invariants']
+
+/**
+ * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value
+ * algebra is enforced by unit tests.
+ */
+const install: InvariantInstaller = () => {}
+
+/**
+ * Register this package's invariant companion.
+ * @param ctx - Cordis context carrying the invariant service.
+ * @returns the installed registration's disposer after setup succeeds.
+ */
+export const apply = (ctx: Context): Promise<() => void> =>
+  Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
+/* jscpd:ignore-end */

+ 29 - 0
packages/util/crypto/tests/uuid.spec.ts

@@ -0,0 +1,29 @@
+/**
+ * The minted UUID's contract: RFC 9562 v4 shape (version and variant bits
+ * pinned), uniqueness across calls, and no dependence on the secure-context
+ * `crypto.randomUUID` — the reason this package exists.
+ */
+import { describe, expect, it, vi } from 'vitest'
+import { randomUUID } from '../src/index.ts'
+
+const V4_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
+
+describe('randomUUID', () => {
+  it('emits RFC 9562 v4 ids', () => {
+    for (let round = 0; round < 64; round += 1) expect(randomUUID()).toMatch(V4_SHAPE)
+  })
+
+  it('emits distinct ids across calls', () => {
+    expect(new Set(Array.from({ length: 64 }, () => randomUUID())).size).toBe(64)
+  })
+
+  it('mints without the platform randomUUID, the insecure-origin shape', () => {
+    const bare = { getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto) }
+    vi.stubGlobal('crypto', bare)
+    try {
+      expect(randomUUID()).toMatch(V4_SHAPE)
+    } finally {
+      vi.unstubAllGlobals()
+    }
+  })
+})

+ 15 - 0
packages/util/crypto/tsconfig.json

@@ -0,0 +1,15 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "lib/types"
+  },
+  "include": [
+    "src"
+  ],
+  "references": [
+    {
+      "path": "../../runtime-diagnostics/invariants"
+    }
+  ]
+}

+ 24 - 0
pnpm-lock.yaml

@@ -1864,6 +1864,9 @@ importers:
 
   packages/client/ui-conversation:
     dependencies:
+      '@deepseek-ai/dsh-util-crypto':
+        specifier: workspace:^
+        version: link:../../util/crypto
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
@@ -4251,6 +4254,9 @@ importers:
 
   packages/experimental/webworker-runtime:
     dependencies:
+      '@deepseek-ai/dsh-util-crypto':
+        specifier: workspace:^
+        version: link:../../util/crypto
       '@noble/hashes':
         specifier: ^2.3.0
         version: 2.3.0
@@ -5127,6 +5133,9 @@ importers:
       '@deepseek-ai/dsh-user-questions':
         specifier: workspace:^
         version: link:../../interaction/user-questions
+      '@deepseek-ai/dsh-util-crypto':
+        specifier: workspace:^
+        version: link:../../util/crypto
       '@deepseek-ai/dsh-workspace':
         specifier: workspace:^
         version: link:../../workspace/workspace
@@ -5316,6 +5325,9 @@ importers:
 
   packages/interaction/commands:
     dependencies:
+      '@deepseek-ai/dsh-util-crypto':
+        specifier: workspace:^
+        version: link:../../util/crypto
       zod:
         specifier: ^4.4.3
         version: 4.4.3
@@ -5552,6 +5564,9 @@ importers:
 
   packages/llm/llm:
     dependencies:
+      '@deepseek-ai/dsh-util-crypto':
+        specifier: workspace:^
+        version: link:../../util/crypto
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
@@ -8374,6 +8389,15 @@ importers:
         specifier: workspace:^
         version: link:../../runtime-diagnostics/invariants
 
+  packages/util/crypto:
+    devDependencies:
+      '@deepseek-ai/cordis':
+        specifier: workspace:^
+        version: link:../../../vendor/cordis
+      '@deepseek-ai/dsh-invariants':
+        specifier: workspace:^
+        version: link:../../runtime-diagnostics/invariants
+
   packages/util/home-paths:
     devDependencies:
       '@deepseek-ai/cordis':

+ 6 - 6
scripts/lint-rule-fingerprint.spec.ts

@@ -18,19 +18,19 @@ interface Profile {
 // snapshot; they do not re-evaluate that deleted baseline or track its preset.
 const profiles = {
   source: {
-    count: 88,
+    count: 89,
     indexes: [0, 1, 4, 5],
-    sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
+    sha256: '3e8b8455827254d6da59afb803d30f062b9fe1264b4df75ce4c8cd6e742b93dd',
   },
   example: {
-    count: 87,
+    count: 88,
     indexes: [0, 1, 2, 4, 5],
-    sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
+    sha256: 'e2b944c028ef1971302d47b2eb0e95c05e740623c7cbaebb4b1312dbf73bf156',
   },
   test: {
-    count: 83,
+    count: 84,
     indexes: [0, 3, 4, 5],
-    sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
+    sha256: '7800bf57f0d689a5623e4ad60315b10173158fb9cf988d3f005cb115f2245691',
   },
 } as const satisfies Record<string, Profile>
 

+ 2 - 0
tsconfig.base.json

@@ -106,6 +106,7 @@
       "@deepseek-ai/dsh-experimental-tool-agent-team/invariant": ["./packages/experimental/tool-agent-team/src/invariant.ts"],
       "@deepseek-ai/dsh-experimental-webworker-runtime/invariant": ["./packages/experimental/webworker-runtime/src/invariant.ts"],
       "@deepseek-ai/dsh-experimental-webworker-packer/invariant": ["./packages/experimental/webworker-packer/src/invariant.ts"],
+      "@deepseek-ai/dsh-util-crypto/invariant": ["./packages/util/crypto/src/invariant.ts"],
       "@deepseek-ai/dsh-*/invariant": [
         "./packages/core/*/src/invariant.ts",
         "./packages/prompt/*/src/invariant.ts",
@@ -241,6 +242,7 @@
       "@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"],
       "@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"],
       "@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"],
+      "@deepseek-ai/dsh-util-crypto": ["./packages/util/crypto/src"],
       "@deepseek-ai/dsh-*": [
         "./packages/core/*/src",
         "./packages/prompt/*/src",

+ 1 - 0
tsconfig.host.json

@@ -129,6 +129,7 @@
     { "path": "./packages/util/native-command" },
     { "path": "./packages/util/home-paths" },
     { "path": "./packages/util/timeout" },
+    { "path": "./packages/util/crypto" },
     { "path": "./packages/util/output-retention" },
     { "path": "./packages/util/atomic-write" },
     { "path": "./packages/attachment/attachment" },