diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml index f055b6f81f..4290762ae1 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md -2026-07-23-session-telemetry-otel-revival.md: 67ab0d28a168422a6b787a6088648746cd6079b6 -2026-07-23-session-telemetry-otel-revival.zh.md: d59dcd07e9879e1c7cafc7a104d4f2515596cab8 +2026-07-23-session-telemetry-otel-revival.md: c981a067efcc7107a7b7a9b9344a59f1a3bfe3d9 +2026-07-23-session-telemetry-otel-revival.zh.md: c79d29687feeb1645b04031cc4c300a6465b1c38 diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md index 67ab0d28a1..c981a067ef 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md @@ -14,7 +14,7 @@ Every deployment that wants harness sessions in an observability stack must hand - **`@deepseek-ai/dsh-session-telemetry`** — the seam. `TelemetryBackend` (`emit`/`flush?`/`shutdown`), the service-registered `Telemetry` form, and `TelemetryCoordinator` owning capture: live adoption with cursor read-back and the per-append firehose (project → `structuredClone` → redact → `emit`, zero I/O), buffer-free on-demand replay from the canonical log, the fixed first-chunk-per-(turn, step) projection, the live `agent/error` relay, and live dispose-time `shutdown` records. - **The `telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten. -- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. Its default `FULL` mode requires `exporter.url`; the later [feedback-gated telemetry decision](2026-08-05-feedback-gated-session-telemetry.md) adds `FEEDBACK_ONLY` and `DISABLED` delivery modes without moving the redaction or backend boundary, while [buffer-free feedback replay](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix. +- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. `DISABLED` is the default and constructs no transport; the [feedback-gated telemetry decision](2026-08-05-feedback-gated-session-telemetry.md) defines the explicit `FULL` and `FEEDBACK_ONLY` delivery modes, which require `exporter.url`, without moving the redaction or backend boundary. [Buffer-free feedback replay](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix. The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry, queueing, and loss policy are the reporting SDK's, configured through passthroughs — delivery is best-effort (at-most-once across a crash), which the READMEs state plainly. @@ -34,4 +34,4 @@ The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry ## Consequences -A deployment adds one `cordis.yml` entry with an OTLP endpoint and gets its session stream in any OTel-compatible stack. `FULL` preserves that behavior by default, `FEEDBACK_ONLY` replays a canonical-log prefix when feedback is recorded, and `DISABLED` constructs no reporting pipeline; removing the entry remains a silent opt-out, while the disabled mode keeps the local feedback warning. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/record` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited. +A deployment adds one `cordis.yml` entry with an OTLP endpoint and explicitly selects `FULL` to stream sessions into an OTel-compatible stack or `FEEDBACK_ONLY` to replay a canonical-log prefix when feedback is recorded. `DISABLED` is the [default](2026-08-10-telemetry-default-off.md) and constructs no reporting pipeline; removing the entry remains a silent opt-out, while the disabled mode keeps the local feedback warning. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/record` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited. diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md index d59dcd07e9..c79d29687f 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md @@ -14,7 +14,7 @@ Status: implemented - **`@deepseek-ai/dsh-session-telemetry`** —— seam 本体。`TelemetryBackend`(`emit`/`flush?`/`shutdown`)、服务注册形态的 `Telemetry`、以及拥有捕获侧的 `TelemetryCoordinator`:带游标回读的实时纳管与逐 append 的 firehose(投影 → `structuredClone` → 脱敏 → `emit`,零 I/O)、从权威日志进行的无缓冲按需回放、固定的每个(轮次、步骤)组合首分片投影、实时 `agent/error` 转发,以及实时 dispose(资源释放)时的 `shutdown` 记录。 - **`telemetry/record` waterfall(瀑布式事件)** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。 -- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端:OTel JS SDK 日志流水线(`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter),经 `exporter`/`processor` passthrough 原样配置。其默认 `FULL` 模式要求 `exporter.url`;后续的[反馈门控遥测决策](2026-08-05-feedback-gated-session-telemetry.md)增加了 `FEEDBACK_ONLY` 与 `DISABLED` 投递模式,但未移动脱敏或后端边界,而[无缓冲反馈回放](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。 +- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端:OTel JS SDK 日志流水线(`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter),经 `exporter`/`processor` passthrough 原样配置。`DISABLED` 是默认值,且不构造任何传输;[反馈门控遥测决策](2026-08-05-feedback-gated-session-telemetry.md)定义了需显式启用的 `FULL` 与 `FEEDBACK_ONLY` 投递模式,这两种模式要求 `exporter.url`,且不移动脱敏或后端边界。[无缓冲反馈回放](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。 边界公理保持不变:harness 的职责止于 `emit()`。批处理、重试、排队与丢失策略属于 reporting SDK,经 passthrough 配置——投递是尽力而为(崩溃时至多一次),两份 README 对此如实陈述。 @@ -34,4 +34,4 @@ Status: implemented ## 后果 -部署方在 `cordis.yml` 加一个带 OTLP endpoint 的 Cordis 配置项即可把会话流接入任何 OTel 兼容体系。`FULL` 默认保留该行为,`FEEDBACK_ONLY` 在记录反馈时回放权威日志前缀,`DISABLED` 则不构造上报流水线;删除该配置项仍是静默退出方式,而禁用模式会保留本地反馈警告。未挂载规则的部署导出的记录与捕获时完全一致,包括文件内容与命令输出中内嵌的任何凭据。因此,跨信任边界的部署必须挂载 `telemetry/record` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是真源。崩溃持久性在上述 outbox 决定重新审议前明确不在范围内。 +部署方在 `cordis.yml` 加一个带 OTLP endpoint 的 Cordis 配置项,并显式选择 `FULL`,即可把会话流接入任何 OTel 兼容体系;选择 `FEEDBACK_ONLY` 则会在记录反馈时回放权威日志前缀。`DISABLED` 是[默认值](2026-08-10-telemetry-default-off.md),且不构造上报流水线;删除该配置项仍是静默退出方式,而禁用模式会保留本地反馈警告。未挂载规则的部署导出的记录与捕获时完全一致,包括文件内容与命令输出中内嵌的任何凭据。因此,跨信任边界的部署必须挂载 `telemetry/record` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是真源。崩溃持久性在上述 outbox 决定重新审议前明确不在范围内。 diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml index 0762a4d131..46d633a37e 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md -2026-07-30-versioned-gui-welcome-onboarding.md: 4707769d4fa9fbf184e09a2e73087dfd326070be -2026-07-30-versioned-gui-welcome-onboarding.zh.md: 002a04b1fc6acc4fc7cfee053bdbbb2abaef047e +2026-07-30-versioned-gui-welcome-onboarding.md: c720b822acc613799681671bfdb6820e4397bbe6 +2026-07-30-versioned-gui-welcome-onboarding.zh.md: d52f971140de339ccc7919355b97ac92f9b86bb3 diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md index 4707769d4f..c720b822ac 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md @@ -12,7 +12,7 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check, **The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. The product welcome registers at order `-100`, while `ui-models` retains only the conditional DeepSeek readiness and credential-routing step at order `0`. -**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice identifies `DSH_TELEMETRY_DISABLED=1` as the telemetry opt-out. +**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice states that session telemetry is [disabled by default](2026-08-10-telemetry-default-off.md) and names the `FEEDBACK_ONLY` and `FULL` opt-in modes. **Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again. diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md index 002a04b1fc..d52f971140 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md @@ -12,7 +12,7 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测 **设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。产品欢迎步骤的顺序为 `-100`,`ui-models` 则只保留顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤。 -**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知明确以 `DSH_TELEMETRY_DISABLED=1` 作为遥测关闭方式。 +**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知说明会话遥测[默认禁用](2026-08-10-telemetry-default-off.md),并列出 `FEEDBACK_ONLY` 和 `FULL` 两种显式启用模式。 **loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml index 6f61384fd8..5423340a17 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md -2026-07-31-web-telemetry-default-mount.md: 3e20fa44666139778e85a8bd5b948ab2df14529b -2026-07-31-web-telemetry-default-mount.zh.md: 97b153255a1e64aba0a686faf1817b69467fb663 +2026-07-31-web-telemetry-default-mount.md: 93bb788bd48ab8c82bd013c8f844e01913f537e4 +2026-07-31-web-telemetry-default-mount.zh.md: d75d3e36f5e4b6c9fab146b28e301cfc3b11f8b6 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md index 3e20fa4466..93bb788bd4 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md @@ -10,23 +10,24 @@ The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry ## Decision -The shared `dsh` base (`apps/cli/config/base.cordis.yml`) mounts the `telemetry-otel` row by default with a baked-in production endpoint, so Web and headless report; the raw-config command also mounts it before applying its required deployment overlay. This is the **internal-testing deployment stance** — reporting is on when an endpoint exists, and users opt out through the environment. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving the backend's three-second shutdown deadline time to drain before the five-second launcher bound. +The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `telemetry-otel` row with a baked-in production endpoint, so every profile has one consistent telemetry capability. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps that row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound. | Ruling | Value | Rationale | |---|---|---| -| Mount surface | base.cordis.yml (raw config + Web + headless) | One deployment stance for every tree that loads the shared base; the raw overlay decides whether that deployment creates sessions | +| Mount surface | `packages/bundle/base/cordis.patch.yml` | One capability row for every profile that loads the shared base | +| Sharing mode | `DSH_TELEMETRY_MODE`, default `DISABLED`; explicit `FULL` or `FEEDBACK_ONLY` opts in | A fresh profile makes no telemetry network request, while internal deployments retain both upload policies | | Endpoint | `DSH_TELEMETRY_OTLP_URL`, default `https://harness-telemetry.deepseeksvc.com/v1/logs` | Internal collector; the env override serves local/dev runs | -| Opt-out switch | any non-empty `DSH_TELEMETRY_DISABLED` (including `0`/`false`) disables | A privacy switch prefers off-by-mistake over on-by-mistake; a row can only be disabled at AppCLIEntry's patch layer (config has no disable semantic, and the switch must precede the load-time `exporter.url` validation) | -| Cadence | `processor.scheduledDelayMillis: 10000` (10s/batch) | Streaming while the session runs, never exit-time-only; a crash loses at most the last unexported interval | +| Hard opt-out | any non-empty `DSH_TELEMETRY_DISABLED` (including `0`/`false`) disables the row | The launcher patch takes effect before load-time transport validation and overrides every configured mode | +| Cadence | `processor.scheduledDelayMillis: 10000` (10s/batch) in uploading modes | Streaming while the session runs, never exit-time-only; a crash loses at most the last unexported interval | | Exit-drain bound | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048` (== maxQueueSize) + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | Ordinary unreachable-collector failure releases in ~1s: timeoutMillis is the per-attempt socket timeout and retry deadline, while one queue-sized batch avoids sequential drain multiplication. The DSH-owned 3s outer bound covers the SDK's preceding unbounded `forceFlush()` wait when the transport Promise never obtains a socket. | | Compression | `compression: gzip` | Event bodies carry full content; cross-datacenter bandwidth | -| CI isolation | top-level `env: DSH_TELEMETRY_DISABLED: '1'` in all 8 GitHub workflows | Every CI channel that boots the web composition (e2e/snapshot/built smokes) must not stream test sessions to the production endpoint | +| CI isolation | top-level `env: DSH_TELEMETRY_DISABLED: '1'` in GitHub workflows | Defense in depth keeps test sessions local even when a job explicitly selects an uploading mode | -The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the deployment-level behavior: an in-test OTLP collector plus a mock LLM server, a real `dsh web` boot, asserting ledger coverage, seq monotonicity, the first-of-step chunk projection, and the ops `shutdown` marker arriving through the SIGINT drain. +The base bundle test pins the shipped `DISABLED` mode expression, the backend suite pins that omitted mode constructs no transport, and the real Loader composition suite explicitly selects each uploading mode when it verifies OTLP delivery. ## Alternatives considered -**No default mount; deployments add the row themselves (continuing the SDK stance).** Rejected for this stage: this repo's web/headless composition IS the internal deployment, and default-on reporting is that deployment's product requirement; the SDK stance survives in the Service Definition packages (unmounted = nothing leaves). +**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives all profiles one patch target without authorizing any upload. **A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat. @@ -34,6 +35,6 @@ The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the depl ## Consequences -- A developer running `dsh web` without a local collector POSTs to the production endpoint every 10s (silent failure when unreachable; no OTel diag logger is registered); local development sets `DSH_TELEMETRY_DISABLED=1` or points `DSH_TELEMETRY_OTLP_URL` locally. -- **No redaction rule is mounted yet**: exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, the remaining identity Resource attributes (hostname / surface; the anonymous user id shipped via the [anonymous-user-id Note](2026-07-31-telemetry-anonymous-user-id.md)), and the usage-metrics track are the explicit follow-ups of this decision. -- Test rigs reusing this tree (e.g. `apps/web/tests/scaffold.ts`) must explicitly disable the row, or fixture sessions stream to whatever collector the environment happens to name. +- A developer running `dsh web` without telemetry configuration makes no telemetry network request. An internal deployment sets `DSH_TELEMETRY_MODE` and may point `DSH_TELEMETRY_OTLP_URL` at another collector. +- **No redaction rule is mounted**: explicitly enabled exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, remaining identity Resource attributes, and usage metrics remain separate deployment work. The anonymous user id ships through the [anonymous-user-id Note](2026-07-31-telemetry-anonymous-user-id.md). +- Test rigs remain local by default; explicit uploading-mode tests provide their own collector and mode. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md index 97b153255a..d75d3e36f5 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.zh.md @@ -10,23 +10,24 @@ Status: implemented ## 决策 -`dsh` 共享 base(`apps/cli/config/base.cordis.yml`)默认挂载 `telemetry-otel` 行,内置生产 endpoint,因此 Web 与 headless 都会上报;原始配置命令也会先挂载该行,再应用其必需的部署 overlay。这是**内部测试期的部署立场**——有 endpoint 就上报,用户可通过环境变量退出。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md),在启动器 5 秒上限到期前,先给后端 3 秒关闭截止时间完成排空。 +共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `telemetry-otel` 配置行,使每个 profile 都具有一致的遥测能力。[默认关闭决策](2026-08-10-telemetry-default-off.md)让该配置行保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。 | 决策项 | 取值 | 理由 | |---|---|---| -| 挂载面 | base.cordis.yml(原始配置 + Web + headless) | 所有加载共享 base 的配置树采用同一个部署立场;原始配置 overlay 决定该部署是否创建会话 | +| 挂载面 | `packages/bundle/base/cordis.patch.yml` | 每个加载共享基础组合包的 profile 都使用同一个能力配置行 | +| 共享模式 | `DSH_TELEMETRY_MODE`,默认 `DISABLED`;显式设置 `FULL` 或 `FEEDBACK_ONLY` 即启用 | 新 profile 不发出遥测网络请求,内部部署仍可使用两种上传策略 | | endpoint | `DSH_TELEMETRY_OTLP_URL`,缺省 `https://harness-telemetry.deepseeksvc.com/v1/logs` | 内部 collector;env 覆盖供本地/联调 | -| 退出开关 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即关 | 隐私向开关取「宁关勿误开」;行级 disable 只能在 AppCLIEntry 的 patch 层做(config 无 disable 语义,且必须先于 `exporter.url` 的加载期校验生效) | -| 上报节奏 | `processor.scheduledDelayMillis: 10000`(10s/批) | 流式回流,非退出才报;崩溃至多丢失最后一个尚未导出间隔内的数据 | +| 硬性退出 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即禁用该配置行 | 启动器 patch 在加载期传输校验之前生效,并覆盖所有已配置模式 | +| 上报节奏 | 上传模式中为 `processor.scheduledDelayMillis: 10000`(10s/批) | 在会话运行期间流式上报,而非仅在退出时上报;崩溃至多丢失最后一个尚未导出间隔内的数据 | | 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048`(与 maxQueueSize 相等) + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | collector 不可达的常规故障会在约 1s 内放行:timeoutMillis 是单次 socket 超时与重试 deadline,使用与队列等大的单批可避免依次排空导致耗时倍增。由 DSH 管理的 3s 外层上限覆盖 SDK 先执行的无界 `forceFlush()` 等待,即传输 Promise 始终无法取得 socket 的情况。 | | 压缩 | `compression: gzip` | 事件 body 含全文,跨机房带宽 | -| CI 隔离 | 全部 8 个 GitHub 工作流顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道(e2e/快照/已构建版本的冒烟测试)不得向生产 endpoint 泄测试会话 | +| CI 隔离 | GitHub 工作流顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | 即使 CI 任务显式选择上传模式,纵深防御也会让测试会话留在本地 | -集成测试 `apps/cli/tests/telemetry-web.e2e.ts`(keyless)钉住部署级行为:测试内 OTLP collector + mock LLM(大语言模型)服务器,真启动 `dsh web`,断言 ledger 覆盖、seq 单调、步骤首个分片投影、以及 SIGINT drain 后 ops `shutdown` 标记到达。 +基础组合包测试固定交付的 `DISABLED` 模式表达式,后端测试套件固定省略模式时不构造传输,真实 Loader 组合测试则在验证 OTLP 投递时显式选择每种上传模式。 ## 考虑过的替代方案 -**默认不挂载,部署方自行加行(SDK 立场的延续)。** 当前阶段不采用:本仓的 web/headless 组合就是内部部署本身,「上报默认开」是这个部署的产品要求;SDK 立场仍由 Service Definition 包保持(不挂 = 零外发)。 +**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为所有 profile 提供同一个 patch 目标,同时不授权任何上传。 **开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。 @@ -34,6 +35,6 @@ Status: implemented ## 后果 -- 无本地 collector 的开发者跑 `dsh web` 会对生产 endpoint 每 10s 发一次 POST(联不通则静默失败,OTel diag logger 未注册);本地开发设 `DSH_TELEMETRY_DISABLED=1` 或 `DSH_TELEMETRY_OTLP_URL` 指本地。 -- **当前尚未挂载任何脱敏规则**:导出即原始捕获副本(用户/助手消息全文、工具参数与工具结果、系统提示词、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、其余身份 Resource 属性(hostname/surface;匿名 user id 已由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地)、使用数据 metrics 轨是本决策明确的后续工作。 -- 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture(测试前置数据)会话会流向 env 里碰巧存在的 collector。 +- 开发者运行没有遥测配置的 `dsh web` 时,不会发出遥测网络请求。内部部署需设置 `DSH_TELEMETRY_MODE`,并可让 `DSH_TELEMETRY_OTLP_URL` 指向其他 collector。 +- **没有挂载任何脱敏规则**:显式启用的导出即原始捕获副本(用户/助手消息全文、工具参数与工具结果、系统提示词、`session.cwd` 本地路径)。跨信任边界前必须先挂载 `telemetry/record` 规则;脱敏规则、其余身份 Resource 属性和使用情况指标仍是独立的部署工作。匿名 user id 由[匿名 user id Note](2026-07-31-telemetry-anonymous-user-id.md)交付。 +- 测试载具默认将数据留在本地;显式启用上传模式的测试提供自己的 collector 和模式。 diff --git a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.i18n.yaml index 63b0efa3aa..ba99863f03 100644 --- a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md -2026-08-05-feedback-gated-session-telemetry.md: 7d923a7e4cf61e8d1119187564b87e4cbb2065b7 -2026-08-05-feedback-gated-session-telemetry.zh.md: e2fa6f9561de6e7295cbf860527c7d7915fec0bf +2026-08-05-feedback-gated-session-telemetry.md: 524eb9defa9981c6d535ff68012d206b9c0cff94 +2026-08-05-feedback-gated-session-telemetry.zh.md: 557e3681f627931560d19172ddd8fa69a2984af1 diff --git a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md index 7d923a7e4c..524eb9defa 100644 --- a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md +++ b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md @@ -6,15 +6,15 @@ English | [中文](2026-08-05-feedback-gated-session-telemetry.zh.md) ## Problem -Session telemetry originally has one mounted behavior: every accepted record enters the reporting backend immediately. Deployments need two stricter policies without replacing the plugin: hold a session's telemetry unless its user records feedback, or disable reporting while still explaining what happens to feedback. The policy must preserve the existing full-export default and the telemetry seam's redaction-before-backend boundary. +Session telemetry originally has one mounted behavior: every accepted record enters the reporting backend immediately. Deployments need two stricter policies without replacing the plugin: hold a session's telemetry unless its user records feedback, or disable reporting while still explaining what happens to feedback. The policy must preserve the telemetry seam's redaction-before-backend boundary. ## Decision `@deepseek-ai/dsh-session-telemetry-otel` exposes the string-valued `TelemetryMode` enum to TypeScript callers and accepts the same three uppercase `mode` values in serialized configuration: -- `FULL` is the default and preserves immediate delivery to the configured OTel pipeline. +- `FULL` explicitly selects immediate delivery to the configured OTel pipeline. - `FEEDBACK_ONLY` reads the canonical session log when `feedback/record` is appended and hands over the unreleased prefix through that exact event. Records appended after that boundary remain local until another feedback event. -- `DISABLED` constructs no exporter, processor, or logger provider. A `feedback/record` listener prints that nothing is shared and the feedback remains local. +- `DISABLED` is the [default](2026-08-10-telemetry-default-off.md), constructs no exporter, processor, or logger provider, and prints that nothing is shared and the feedback remains local when it observes `feedback/record`. The generic telemetry coordinator owns `live` and `on-demand` capture. Live capture projects, clones, redacts, and hands each event to the backend on the session firehose. On-demand capture registers no continuous capture listeners; `captureSession(session, throughSeq)` reads the canonical log from the handoff cursor through an inclusive boundary, then projects, clones, redacts, and hands over that prefix. The cursor advances only for handed-over records. The [buffer-free replay decision](../simplification/2026-08-06-buffer-free-feedback-telemetry.md) owns why the on-demand path uses the canonical log instead of copied records. @@ -32,4 +32,4 @@ Mode resolution is a closed, fail-before-setup check: an unknown direct-construc ## Consequences -`FULL` remains source- and wire-compatible with the original default. `FEEDBACK_ONLY` adds no telemetry-owned per-event buffer before feedback; direct service calls and non-canonical feedback events upload nothing, and a crash before feedback uploads nothing from that prefix. Replay applies the redaction policy mounted when feedback is recorded and excludes operational records that do not exist in the canonical log. Feedback-only streams therefore carry neither `agent-error` nor `shutdown` records, and shutdown absence is not a crash signal. Each later feedback captures the suffix accumulated since the previous boundary. `DISABLED` can omit `exporter.url`, does no reporting work, and keeps feedback only in the canonical session log. +`FULL` retains the original source and wire behavior as an explicit opt-in. `FEEDBACK_ONLY` adds no telemetry-owned per-event buffer before feedback; direct service calls and non-canonical feedback events upload nothing, and a crash before feedback uploads nothing from that prefix. Replay applies the redaction policy mounted when feedback is recorded and excludes operational records that do not exist in the canonical log. Feedback-only streams therefore carry neither `agent-error` nor `shutdown` records, and shutdown absence is not a crash signal. Each later feedback captures the suffix accumulated since the previous boundary. `DISABLED` can omit `exporter.url`, does no reporting work, and keeps feedback only in the canonical session log. diff --git a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md index e2fa6f9561..557e3681f6 100644 --- a/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md @@ -6,15 +6,15 @@ Status: implemented ## 问题 -会话遥测原本只有一种已挂载行为:每条已接受记录都立即进入上报后端。部署方需要两种更严格的策略,且不替换插件:只有用户记录反馈时才释放该会话的遥测,或禁用上报并仍向用户说明反馈的去向。该策略必须保留现有的全量导出默认值,以及遥测 seam 在记录抵达后端之前脱敏的边界。 +会话遥测原本只有一种已挂载行为:每条已接受记录都立即进入上报后端。部署方需要两种更严格的策略,且不替换插件:只有用户记录反馈时才释放该会话的遥测,或禁用上报并仍向用户说明反馈的去向。该策略必须保留遥测 seam 在记录抵达后端之前脱敏的边界。 ## 决策 `@deepseek-ai/dsh-session-telemetry-otel` 向 TypeScript 调用方公开以字符串为值的 `TelemetryMode` 枚举,并在序列化配置中接受相同的三个大写 `mode` 值: -- `FULL` 是默认值,保留向已配置 OTel 流水线的即时投递。 +- `FULL` 显式选择向已配置 OTel 流水线即时投递。 - `FEEDBACK_ONLY` 在追加 `feedback/record` 时读取权威会话日志,并交接截至该事件的未释放前缀。该边界后追加的记录会留在本地,直到另一个反馈事件。 -- `DISABLED` 不构造导出器、处理器或日志提供方。`feedback/record` 监听器会输出警告,说明什么都不会共享,且反馈仍留在本地。 +- `DISABLED` 是[默认值](2026-08-10-telemetry-default-off.md),不构造导出器、处理器或日志提供方,并在观察到 `feedback/record` 时输出警告,说明什么都不会共享,且反馈仍留在本地。 通用遥测协调器拥有 `live` 与 `on-demand` 捕获。实时捕获在会话 firehose 上投影、深拷贝、脱敏每个事件,并将其交给后端。按需捕获不注册持续捕获监听器;`captureSession(session, throughSeq)` 从 handoff 游标起读取权威日志,直至含边界的指定序列号,然后投影、深拷贝、脱敏并交接该前缀。游标只为已交接记录推进。[无缓冲回放决策](../simplification/2026-08-06-buffer-free-feedback-telemetry.md)说明了按需路径为何使用权威日志而非记录副本。 @@ -32,4 +32,4 @@ Status: implemented ## 后果 -`FULL` 与原有默认值保持源码及协议兼容。`FEEDBACK_ONLY` 在反馈前不增加遥测自有的逐事件缓冲;直接服务调用与非权威反馈事件均不上传任何内容,且反馈前发生崩溃时,该前缀也不上传任何内容。回放使用记录反馈时挂载的脱敏策略,并排除权威日志中不存在的运维记录。因此,仅反馈的流既不携带 `agent-error` 记录,也不携带 `shutdown` 记录,而缺少 shutdown 不是崩溃信号。每个后续反馈都会捕获从上一个边界起累积的后缀。`DISABLED` 可省略 `exporter.url`,不执行任何上报工作,并仅在权威会话日志中保留反馈。 +`FULL` 作为显式启用模式保留原有的源码与协议行为。`FEEDBACK_ONLY` 在反馈前不增加遥测自有的逐事件缓冲;直接服务调用与非权威反馈事件均不上传任何内容,且反馈前发生崩溃时,该前缀也不上传任何内容。回放使用记录反馈时挂载的脱敏策略,并排除权威日志中不存在的运维记录。因此,仅反馈的流既不携带 `agent-error` 记录,也不携带 `shutdown` 记录,而缺少 shutdown 不是崩溃信号。每个后续反馈都会捕获从上一个边界起累积的后缀。`DISABLED` 可省略 `exporter.url`,不执行任何上报工作,并仅在权威会话日志中保留反馈。 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml new file mode 100644 index 0000000000..4817fe52bd --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.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 .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md +2026-08-10-telemetry-default-off.md: e4a075fb6eee606057ffe1b953af525169d9cb80 +2026-08-10-telemetry-default-off.zh.md: 4f3b27e76fcd895269b0934f3623ce355e377913 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md new file mode 100644 index 0000000000..e4a075fb6e --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md @@ -0,0 +1,31 @@ +# Agent Note: Telemetry requires explicit opt-in + +Status: implemented + +English | [中文](2026-08-10-telemetry-default-off.zh.md) + +## Problem + +DeepSeek Harness has two outbound telemetry feeds. The session OTel backend could export complete session content, tool data, prompts, and workspace paths when its mode was omitted, while the dsh-sdk launcher feed reported by default. A fresh installation therefore permitted outbound reporting without a positive deployment choice. + +## Decision + +Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and empty values resolve to `DISABLED`. `@deepseek-ai/dsh-session-telemetry-otel` also resolves an omitted `mode` to `DISABLED`, which constructs no OTel provider, processor, or exporter and leaves feedback in the local session log. The shared dsh base keeps the backend row mounted so disabled feedback can still explain that nothing was shared. A deployment opts into Session Log sharing through `FULL` or `FEEDBACK_ONLY`; only `FULL` also permits dsh-sdk launcher reporting. Any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative pre-load hard opt-out. The [default-mount decision](2026-07-31-web-telemetry-default-mount.md) continues to own the endpoint, batching cadence, and exit-drain settings. + +The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. This rule supersedes only the default-on launcher consent in the [SDK follow-up proposal](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md); its other capabilities remain proposed. + +The versioned Web welcome notice states that Session Log upload is off by default and names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices. Its version changes with that material privacy statement so every profile acknowledges the current copy. + +## Alternatives considered + +**Keep opt-out defaults and improve disclosure.** Rejected because disclosure does not make a missing configuration a positive authorization to send data, especially when session telemetry can contain complete local content. + +**Default session telemetry to `FEEDBACK_ONLY`.** Rejected because recording feedback would still trigger an upload without a deployment explicitly enabling outbound reporting. The default must keep both the session and its feedback local. + +**Add project-level consent markers.** Rejected because `DSH_TELEMETRY_MODE` already expresses consent for both feeds; another configuration entry would create conflicting settings and require launcher-specific parsing. + +**Remove both telemetry implementations.** Rejected because internal deployments still need explicit `FULL` and feedback-gated reporting, and the launcher feed remains useful under `FULL`. + +## Consequences + +Fresh profiles and projects make no telemetry network request. Internal deployments select one mode for both feeds: `FEEDBACK_ONLY` permits only feedback-triggered Session Log sharing, while `FULL` also enables launcher reporting. The existing hard opt-out remains effective, and uploading modes retain their endpoint validation, redaction responsibility, batching, and shutdown behavior. diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md new file mode 100644 index 0000000000..4f3b27e76f --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 遥测必须显式启用 + +Status: implemented + +[English](2026-08-10-telemetry-default-off.md) | 中文 + +## 问题 + +DeepSeek Harness 有两路出站遥测数据流。会话 OTel 后端在省略 `mode` 时可能导出完整会话内容、工具数据、提示词和工作区路径,而 dsh-sdk 启动器数据流默认上报。因此,全新安装无需部署方明确选择便允许向外上报。 + +## 决策 + +两路数据流都使用 `DSH_TELEMETRY_MODE` 作为正向授权配置。未设置和空值都解析为 `DISABLED`。`@deepseek-ai/dsh-session-telemetry-otel` 也将省略的 `mode` 解析为 `DISABLED`;该模式不构造 OTel 提供方、处理器或导出器,并将反馈留在本地会话日志中。dsh 共享基础配置继续挂载后端配置行,使禁用模式仍可在记录反馈时说明没有共享任何内容。部署方通过 `FULL` 或 `FEEDBACK_ONLY` 显式启用 Session Log 共享;只有 `FULL` 还允许 dsh-sdk 启动器上报。任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的加载前硬性退出开关。[默认挂载决策](2026-07-31-web-telemetry-default-mount.md)继续负责 endpoint、批处理节奏和退出排空设置。 + +dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。此规则仅取代 [SDK 后续功能提案](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md)中启动器默认允许上报的规则;其余能力仍处于提案状态。 + +带版本的 Web 欢迎通知说明会话日志上传默认关闭,并将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。 + +## 考虑过的替代方案 + +**保留默认退出机制并改进披露。** 不采用:披露不能让缺少配置构成发送数据的明确授权,尤其是会话遥测可能包含完整的本地内容。 + +**将会话遥测默认设为 `FEEDBACK_ONLY`。** 不采用:即使部署方没有显式启用向外上报,记录反馈仍会触发上传。默认值必须让会话及其反馈都留在本地。 + +**添加项目级授权标记。** 不采用:`DSH_TELEMETRY_MODE` 已能表达两路数据流的授权;另一个配置项会产生冲突设置,并需要启动器专用的解析逻辑。 + +**删除两种遥测实现。** 不采用:内部部署仍需要显式启用 `FULL` 与反馈触发的上报;在 `FULL` 下,启动器数据流也仍有用。 + +## 后果 + +全新 profile 和项目不发出任何遥测网络请求。内部部署为两路数据流选择一个模式:`FEEDBACK_ONLY` 只允许由反馈触发的 Session Log 共享,`FULL` 还会启用启动器上报。现有硬性退出继续生效,上传模式也保留 endpoint 校验、脱敏责任、批处理和关闭行为。 diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml index 7084931b1d..eb5cf10d77 100644 --- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md -2026-07-17-sdk-follow-up-capabilities.md: f14d46a61f5fd3e64067441c2f8340cf94746a79 -2026-07-17-sdk-follow-up-capabilities.zh.md: 2e5efba340d503d2445e408bfc43ee0d6c6bec3c +2026-07-17-sdk-follow-up-capabilities.md: 3f264bab6785458aaab14f22bfbba46363d729a3 +2026-07-17-sdk-follow-up-capabilities.zh.md: 4a4308c0bf5f1d28f1d125f8d35bfc6dabc944f7 diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md index f14d46a61f..3f264bab67 100644 --- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md @@ -55,13 +55,13 @@ This proposal concerns dependencies of developer-owned SDK projects. Standalone Telemetry wraps the `create-sdk` initializer and the `dsh-sdk` launcher command lifecycle because project initialization, plugin creation, and build do not reliably boot Cordis. One event records the command name, duration, success, a random per-user anonymous identifier, and redacted `cordis.yml` and `package.json` text when those project files are eligible. -Reporting is enabled unless a present telemetry config entry is explicitly disabled. `DO_NOT_TRACK` and CI deny reporting regardless of project configuration. A missing `cordis.yml` does not itself deny the event, but `package.json` content is included only when `cordis.yml` establishes that the directory is an SDK project. +The [explicit-opt-in decision](../../implemented/feature/2026-08-10-telemetry-default-off.md) supersedes this proposal's default-on consent rule. Launcher reporting reads the shared `DSH_TELEMETRY_MODE` and is enabled only under `FULL`; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny reporting. `package.json` content is included only when `cordis.yml` establishes that the directory is an SDK project. ### Safety and delivery The payload builder never reads `.env`. It redacts secret-shaped keys and values, known token forms, PEM blocks, URL credentials, and high-entropy opaque strings in the two eligible text files. Redaction is a safety backstop rather than a guarantee; SDK projects must keep credentials in `.env`. -The reporter uses a fixed endpoint and resolves every send path without throwing. Command dispatch records success or failure in a `finally` path, starts reporting after the command outcome is known, and drains within a bounded interval. Consent parsing, payload construction, storage, or network failures are swallowed only at this telemetry boundary and never alter the command's exit code. +The reporter uses a fixed endpoint and resolves every send path without throwing. Command dispatch records success or failure in a `finally` path, starts reporting after the command outcome is known, and drains within a bounded interval. Consent resolution, payload construction, storage, or network failures are swallowed only at this telemetry boundary and never alter the command's exit code. ## Interactive workflow testing @@ -72,7 +72,6 @@ One or two optional real-PTY smoke tests may cover the shipped binary and TTY gu ## Deferred work - Extend the headless create specification to express local `plugin` or `tool` scaffolding instead of defaulting that interactive choice to none. -- Expose the telemetry opt-out in create and config while preserving the consent representation in which only a disabled telemetry entry is written. - Define whether GitHub source dependencies must be prebuilt or may run package-manager-controlled preparation scripts, and surface the policy before installation. - Replace the telemetry package's `.invalid` endpoint placeholder with the production endpoint before release. @@ -99,7 +98,7 @@ One or two optional real-PTY smoke tests may cover the shipped binary and TTY gu - Create runs without a TTY from a complete structured input, emits only NDJSON on stdout under `--json`, and reports missing required input as `action-required` without writing a partial project. - Create and config resolve the same feature-plan contract through the shared question, feature-configuration, and project-editing code paths. - `dsh-sdk create ` uses the selected project package manager, mounts the dependency name that operation actually added, and fails loudly when no new dependency can be identified. -- The initializer and every `dsh-sdk` command reach one best-effort telemetry completion path; an explicit disabled entry, `DO_NOT_TRACK`, or CI prevents delivery, and telemetry failures never change the command result. +- The initializer and every `dsh-sdk` command reach one best-effort telemetry completion path; only `DSH_TELEMETRY_MODE=FULL` permits delivery, and telemetry failures never change the command result. - Telemetry never reads `.env`, withholds unrelated `package.json` content when no `cordis.yml` exists, redacts both eligible text payloads, and uses an identifier unrelated to git metadata. - Interactive tests cover create and config decisions through injected interaction and assert committed project files; any real-PTY coverage remains a narrow smoke layer. - The agent skill documents the public structured-input and event contracts without depending on private package exports. @@ -107,7 +106,7 @@ One or two optional real-PTY smoke tests may cover the shipped binary and TTY gu ## Risks - Full redacted `cordis.yml` and `package.json` text still reveals plugin and dependency names, URLs, paths, and configuration values to the endpoint operator, and heuristic redaction can miss a secret. -- Default-on reporting may surprise developers when no telemetry entry exists; the CLI must make the opt-out discoverable before release. +- `FULL` opts into both complete Session Log sharing and launcher project-text reporting; deployments cannot enable those feeds independently. - A package-manager add can change `package.json`, the lockfile, and installed files before `ProjectEditSession` mounts the plugin, so a later mount failure can leave dependency changes that require manual recovery. - GitHub dependencies may execute preparation or lifecycle code according to package-manager policy; an unresolved build policy is a supply-chain and reproducibility risk. - Injected prompt tests do not prove raw-mode, signal, or repaint behavior in a real terminal; the optional smoke layer must cover only those residual contracts. @@ -116,5 +115,4 @@ One or two optional real-PTY smoke tests may cover the shipped binary and TTY gu - [Vercel Eve](https://github.com/vercel/eve) and [Vercel Labs Skills](https://github.com/vercel-labs/skills) for the distinction between a headless initializer and skill distribution. - [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec), [pnpm add](https://pnpm.io/cli/add), and [Yarn add](https://yarnpkg.com/cli/add) for package-manager-native sources. -- [`DO_NOT_TRACK`](https://donottrack.sh/) for the environment-level opt-out convention. - [Clack](https://github.com/bombshell-dev/clack) and [Vitest snapshots](https://vitest.dev/guide/snapshot) for injected prompts and generated-file assertions. diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md index 2e5efba340..4a4308c0bf 100644 --- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md @@ -55,7 +55,7 @@ Create 和 config 使用相同的功能计划形状。create 通过上述命令 遥测包住 `create-sdk` 初始化命令与 `dsh-sdk` launcher 的命令生命周期,因为工程初始化、插件创建和 build 都不会稳定地启动 Cordis。每个事件记录命令名、时长、成败、随机生成的用户级匿名标识符,以及符合条件时经过脱敏的 `cordis.yml` 与 `package.json` 文本。 -除非当前存在的遥测配置项被明确禁用,否则允许上报。`DO_NOT_TRACK` 和 CI 无论工程配置如何都禁止上报。缺少 `cordis.yml` 本身不会禁止事件,但只有 `cordis.yml` 能证明目录是 SDK 工程时,遥测内容才包含 `package.json` 文本。 +[显式启用决策](../../implemented/feature/2026-08-10-telemetry-default-off.md)取代了本提案默认允许上报的许可规则。启动器上报读取共享的 `DSH_TELEMETRY_MODE`,只在 `FULL` 下启用;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝上报。只有 `cordis.yml` 能证明目录是 SDK 工程时,遥测内容才包含 `package.json` 文本。 ### 安全与传输 @@ -72,7 +72,6 @@ Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入 ## 延后工作 - 扩展 headless create 规格,使其能表达本地 `plugin` 或 `tool` 脚手架,而不是把该交互选择默认为 none。 -- 在 create 和 config 中公开遥测关闭选项,同时保留只有禁用时才写入遥测配置项的上报许可表示。 - 明确 GitHub 来源依赖必须预先构建,还是允许运行由包管理器控制的 preparation script(准备脚本),并在安装前向用户展示该策略。 - 发布前把遥测包中的 `.invalid` endpoint 占位符替换为生产端点。 @@ -99,7 +98,7 @@ Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入 - Create 能依据完整结构化输入在没有 TTY 时运行;使用 `--json` 时 stdout 只输出 NDJSON;缺少必答输入时通过 `action-required` 报告,且不写入部分工程。 - Create 和 config 通过共享的问题、功能配置和工程编辑代码路径解析相同的功能计划约定。 - `dsh-sdk create ` 使用工程选定的包管理器,挂载该操作实际新增的依赖名;无法识别新增依赖时快速失败。 -- 初始化命令与每个 `dsh-sdk` 命令都进入同一条尽力而为的遥测收尾路径;明确禁用的配置项、`DO_NOT_TRACK` 或 CI 会阻止传输,遥测失败绝不改变命令结果。 +- 初始化命令与每个 `dsh-sdk` 命令都进入同一条尽力而为的遥测收尾路径;只有 `DSH_TELEMETRY_MODE=FULL` 才允许传输,遥测失败绝不改变命令结果。 - 遥测绝不读取 `.env`;没有 `cordis.yml` 时不发送无关的 `package.json` 内容;两个符合条件的文本都经过脱敏;匿名标识符与 git 元数据无关。 - 交互测试通过注入交互覆盖 create 和 config 决策,并断言已提交的工程文件;真实 PTY 覆盖只作为窄范围冒烟层。 - Agent skill 说明公开的结构化输入与事件约定,不依赖包的私有导出。 @@ -107,7 +106,7 @@ Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入 ## 风险 - 即使经过脱敏,完整的 `cordis.yml` 与 `package.json` 文本仍会向 endpoint 运营方暴露插件名、依赖名、URL、路径和配置值;启发式脱敏也可能漏掉 secret。 -- 没有遥测配置项时默认上报可能让开发者意外;发布前 CLI 必须让关闭方法易于发现。 +- `FULL` 会同时启用完整 Session Log 共享和启动器工程文本上报;部署方无法独立启用这两路数据流。 - 在 `ProjectEditSession` 挂载插件前,包管理器的 add 操作已经可能修改 `package.json`、lockfile 和安装文件;后续挂载失败会留下需要手工恢复的依赖改动。 - GitHub 依赖可能按包管理器策略执行 preparation 或 lifecycle script;尚未解决的构建策略会带来供应链与可复现性风险。 - 注入提示词交互的测试无法证明真实终端中的 raw mode、signal 或重绘行为;可选冒烟层只应覆盖这些残余约定。 @@ -116,5 +115,4 @@ Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入 - [Vercel Eve](https://github.com/vercel/eve) 与 [Vercel Labs Skills](https://github.com/vercel-labs/skills) 用于区分 headless 初始化命令与 skill 分发。 - [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec)、[pnpm add](https://pnpm.io/cli/add)和 [Yarn add](https://yarnpkg.com/cli/add)说明包管理器原生来源。 -- [`DO_NOT_TRACK`](https://donottrack.sh/)定义环境级关闭约定。 - [Clack](https://github.com/bombshell-dev/clack) 和 [Vitest snapshots](https://vitest.dev/guide/snapshot) 说明注入提示词交互与生成文件断言。 diff --git a/README.i18n.yaml b/README.i18n.yaml index 35cb82e776..9f0c979e45 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 README.md -README.md: 3174630d021b3868986d6ad9989d257fe8ac29fb -README.zh.md: 377ea6372a9a531c1400d08b0ef33792b452dc65 +README.md: 65b51f60bfaafff71d7eb4a8f34a57f3dd3ed12f +README.zh.md: 73c17b84ff3ed90e0d1a0ed94454c405e2e0c8f2 diff --git a/README.md b/README.md index 3174630d02..65b51f60bf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It uses an architecture where **everything is a plugin**. DeepSeek Harness is under internal testing. Features and interfaces may change. -The internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group. +Session Logs stay local by default. Set `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` to share a Session Log only when submitting feedback, or `DSH_TELEMETRY_MODE=FULL` to upload continuously. Send feedback through the internal WeChat group. ## Install diff --git a/README.zh.md b/README.zh.md index 377ea6372a..73c17b84ff 100644 --- a/README.zh.md +++ b/README.zh.md @@ -10,7 +10,7 @@ DeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 DeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。 -为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。 +Session Log 默认留在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 可仅在提交反馈时共享 Session Log,设置 `DSH_TELEMETRY_MODE=FULL` 可持续上传。请通过内部企业微信群反馈问题和建议。 ## 安装 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 6aa39844d0..baa822d2eb 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/cli/reference/README.md -README.md: 7f0bd7b0bd50482b3ba7ee95adf6aaf1defb6d28 -README.zh.md: 55f23c5644b8b063ef12fb56fa587ff5d2eb21a2 +README.md: ec907fa7a497e795a760cdf251054020d72f9d15 +README.zh.md: dea19856eb8618614f41c6ba7a838efbf545ba4c diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 7f0bd7b0bd..ec907fa7a4 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -65,9 +65,9 @@ New sessions default to the `workspace-write` permission preset. Bash and filesy ## Shared deployment behavior -The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it. +The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and disabled session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it. -Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision. +Session telemetry stays local by default. `DSH_TELEMETRY_MODE=FULL` streams every projected session event as OTLP/HTTP logs, while `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` uploads a session-log suffix only when feedback is recorded. `DSH_TELEMETRY_OTLP_URL` selects another collector, and any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative hard opt-out. The shipped base has no telemetry redaction rule, so explicitly enabled exports can contain message text, tool arguments and results, and workspace paths; the [default-off Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md) owns that deployment decision. Install external plugin bundles through `dsh plugin --profile add `. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 55f23c5644..dea19856eb 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -65,9 +65,9 @@ dsh web --dump-config ## 共享部署行为 -基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。 +基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和已禁用的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。 -会话事件默认作为 OTLP/HTTP 日志流式发送。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED` 都会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责。 +会话遥测默认留在本地。`DSH_TELEMETRY_MODE=FULL` 将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 则仅在记录反馈时上传会话日志后缀。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector,任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的硬性退出开关。随附基础配置没有遥测脱敏规则,因此显式启用的导出可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[默认关闭 Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md)负责。 通过 `dsh plugin --profile add ` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。 diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index e4a719379e..b312589f27 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -77,7 +77,7 @@ export const PROFILE_ROOT_FILENAME = 'cordis.yml' * switch set. * @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset). * @param hasRow - whether the composition carries the telemetry row. - * @returns the disable patch, or `undefined` when telemetry stays enabled or is not mounted. + * @returns the disable patch, or `undefined` when no hard-disable patch is required. */ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined { if ((disabledEnv ?? '') === '' || !hasRow) return undefined diff --git a/apps/cli/tests/telemetry-switch.spec.ts b/apps/cli/tests/telemetry-switch.spec.ts index 0f44819564..efb36fc2d6 100644 --- a/apps/cli/tests/telemetry-switch.spec.ts +++ b/apps/cli/tests/telemetry-switch.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { resolveTelemetryPatch } from '../src/profile-boot.ts' describe('resolveTelemetryPatch', () => { - it('keeps telemetry enabled when the switch is unset or empty', () => { + it('preserves the configured telemetry mode when the hard-disable switch is unset or empty', () => { expect(resolveTelemetryPatch(undefined, true)).toBeUndefined() expect(resolveTelemetryPatch('', true)).toBeUndefined() }) diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md index 33e3d79fce..27e1935b23 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md @@ -3,7 +3,7 @@ - paragraph: 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。 - blockquote: “如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。 - paragraph: - - text: 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,可以设置环境变量 DSH_TELEMETRY_DISABLED=1。另外, + - text: 内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL。另外, - strong: 如果您有任何反馈与建议,请在企业微信群中留言告诉我们 - text: 。每一条反馈,都会帮助我们把它打磨得更好。 - button "继续" diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 02e95f011e..99bd4e7a55 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: 10078dea5221cfe76b1c028ef83216f5a4575940 -config-catalog.zh.md: 38ed9f94bee9b0c11f5ea4dad4ba48532cce6450 +config-catalog.md: 8fb3f7fdef7d3fe46d462e4cf85de137f7d5fa56 +config-catalog.zh.md: 434f627265dd6181326dbc6d83c95f032c4bb22f diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 10078dea52..8fb3f7fdef 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1529,7 +1529,7 @@ Requires: `sessions` * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { - /** Sharing policy; defaults to immediate `FULL` delivery. */ + /** Sharing policy; defaults to local-only `DISABLED` behavior. */ mode?: TelemetryMode /** * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 38ed9f94be..434f627265 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1531,7 +1531,7 @@ export interface Config { * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { - /** Sharing policy; defaults to immediate `FULL` delivery. */ + /** Sharing policy; defaults to local-only `DISABLED` behavior. */ mode?: TelemetryMode /** * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 66612f70fd..51a9a35ca6 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -120,14 +120,12 @@ - id: session-projection name: '@deepseek-ai/dsh-session-projection' - # Session telemetry, on for every dsh mode: mirrors every session-log - # event (assistant/chunk projected to first-of-step) plus ops markers onto - # OTLP/HTTP log records, streaming on the batch processor's cadence - # (10s/batch here) — not at exit; a crash loses at most the last unexported - # interval. No telemetry/record redaction rule is mounted yet, so exports - # are the raw captured copy; the deployment stance, env seams, and - # follow-ups are pinned in the web-telemetry-default-mount Agent Note. - # DSH_TELEMETRY_OTLP_URL overrides the production endpoint, and a non-empty + # Session telemetry is mounted but disabled by default. DSH_TELEMETRY_MODE + # explicitly opts into FULL or FEEDBACK_ONLY reporting; uploading mirrors + # session-log records onto OTLP/HTTP logs with no telemetry/record redaction + # rule, so exports are the raw captured copy. The deployment stance, env + # seams, and follow-ups are pinned in the default-off Agent Note. + # DSH_TELEMETRY_OTLP_URL overrides the production endpoint. A non-empty # DSH_TELEMETRY_DISABLED — any value, including '0'/'false' — opts the # process out (the launchers patch the row disabled; config cannot disable # a row). Exports carry the harness home's anonymous user id ($DSH_HOME/.userid, @@ -144,6 +142,7 @@ - id: telemetry-otel name: '@deepseek-ai/dsh-session-telemetry-otel' config: + mode: !!js process.env.DSH_TELEMETRY_MODE || 'DISABLED' shutdownTimeoutMillis: 3000 exporter: url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs' diff --git a/packages/bundle/base/tests/base.spec.ts b/packages/bundle/base/tests/base.spec.ts index 2da84a0931..cf38af88ce 100644 --- a/packages/bundle/base/tests/base.spec.ts +++ b/packages/bundle/base/tests/base.spec.ts @@ -23,11 +23,14 @@ describe('dsh-base bundle', () => { ) expect(Array.isArray(parsed)).toBe(true) // The base layer is one insert list over the empty profile root. - const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap( + const rows = (parsed as { insert?: { id?: string; config?: Record }[] }[]).flatMap( patch => patch.insert ?? [], ) expect(rows.length).toBeGreaterThan(50) expect(rows.some(row => row.id === 'agent-loop')).toBe(true) + expect(rows.find(row => row.id === 'telemetry-otel')?.config?.['mode']).toEqual({ + __jsExpr: "process.env.DSH_TELEMETRY_MODE || 'DISABLED'", + }) }) it('ships the Windows platform layer as the confined pwsh roster over the ACL runner chain', () => { diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 8d7a5f1d4b..3366f440bb 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -2,5 +2,5 @@ # 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/client/ui-settings-general/README.md -README.md: ab27e073dc76335efc619f56365d1705007f7ef2 -README.zh.md: 16ff5604bee5425569b783e27a29699344f630e3 +README.md: 4fa8f0a854285e42fec415e4bcd7182dde745e2a +README.zh.md: e923da7ce27d5bc6a25ee44269acfd68aeb74983 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index ab27e073dc..4fa8f0a854 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -6,7 +6,7 @@ Settings ownerless-copy and product-onboarding plugin: registers everything on t A loopback browser loads the provider's `hasDocument` capability through `settings.describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action sends the pathless, loopback-only `settings.openDocument` request; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Remote browsers never register the action and never issue the privileged settings read. -`src/onboarding-copy.ts` is the single editable owner of the complete notice plus `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese copy. The Host half registers `ui-onboarding` in the user-settings seam. A loopback browser compares `welcomeNoticeVersion` for exact equality and writes the current value only after Continue succeeds. The path mutation is idempotent across tabs and preserves sibling settings, while `host/settings-changed` makes an externally acknowledged notice advance without a reload. A non-loopback browser cannot access the privileged settings API: it still presents the notice, but Continue advances only the current browser process and a reload presents the notice again. A different version deliberately presents the notice again. The welcome page preserves every authored paragraph, gives the requested clause in the final paragraph the sole emphasis, initially focuses the title, and has no close, Escape, mask-click, or secondary path. None of its copy or acknowledgement enters a Session log or model request. The notice identifies `DSH_TELEMETRY_DISABLED=1` as the telemetry opt-out. +`src/onboarding-copy.ts` is the single editable owner of the complete notice plus `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese copy. The Host half registers `ui-onboarding` in the user-settings seam. A loopback browser compares `welcomeNoticeVersion` for exact equality and writes the current value only after Continue succeeds. The path mutation is idempotent across tabs and preserves sibling settings, while `host/settings-changed` makes an externally acknowledged notice advance without a reload. A non-loopback browser cannot access the privileged settings API: it still presents the notice, but Continue advances only the current browser process and a reload presents the notice again. A different version deliberately presents the notice again. The welcome page preserves every authored paragraph, gives the requested clause in the final paragraph the sole emphasis, initially focuses the title, and has no close, Escape, mask-click, or secondary path. None of its copy or acknowledgement enters a Session log or model request. The notice states that session telemetry is disabled by default and names the `FEEDBACK_ONLY` and `FULL` opt-in modes. ## Model Experience diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index 16ff5604be..e923da7ce2 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -6,7 +6,7 @@ 回环浏览器通过 `settings.describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染**打开配置文件**。该操作发送无路径参数且仅限回环访问的 `settings.openDocument` 请求;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。远程浏览器从不注册该操作,也从不发起这项特权设置读取。 -`src/onboarding-copy.ts` 是完整通知文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文文案。宿主端在用户设置 seam 中注册 `ui-onboarding`。回环浏览器会比较 `welcomeNoticeVersion` 是否精确相等,仅在「继续」操作成功后写入当前值。该路径变更在不同标签页间幂等,并会保留同级设置;`host/settings-changed` 则让页面在通知被外部确认后,无需重新加载即可推进。非回环浏览器不能访问受保护的设置 API:它仍会显示通知,但「继续」只推进当前浏览器进程,重新加载后会再次显示通知。版本不同时,系统也会有意重新显示通知。欢迎页保留原文的每个段落,仅强调最后一段中指定的句段,初始焦点落在标题上,并且没有关闭操作、Escape、点击遮罩或次要操作路径。其文案和确认状态均不会进入会话日志或模型请求。通知明确以 `DSH_TELEMETRY_DISABLED=1` 作为遥测关闭方式。 +`src/onboarding-copy.ts` 是完整通知文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文文案。宿主端在用户设置 seam 中注册 `ui-onboarding`。回环浏览器会比较 `welcomeNoticeVersion` 是否精确相等,仅在「继续」操作成功后写入当前值。该路径变更在不同标签页间幂等,并会保留同级设置;`host/settings-changed` 则让页面在通知被外部确认后,无需重新加载即可推进。非回环浏览器不能访问受保护的设置 API:它仍会显示通知,但「继续」只推进当前浏览器进程,重新加载后会再次显示通知。版本不同时,系统也会有意重新显示通知。欢迎页保留原文的每个段落,仅强调最后一段中指定的句段,初始焦点落在标题上,并且没有关闭操作、Escape、点击遮罩或次要操作路径。其文案和确认状态均不会进入会话日志或模型请求。通知说明会话遥测默认禁用,并列出 `FEEDBACK_ONLY` 和 `FULL` 两种显式启用模式。 ## 模型体验 diff --git a/packages/client/ui-settings-general/src/onboarding-copy.ts b/packages/client/ui-settings-general/src/onboarding-copy.ts index 061fc06872..e38ff3203d 100644 --- a/packages/client/ui-settings-general/src/onboarding-copy.ts +++ b/packages/client/ui-settings-general/src/onboarding-copy.ts @@ -8,7 +8,7 @@ export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion' * Bump only when the notice changes materially and every user should see it * again. The acknowledgement is compared for exact equality. */ -export const WELCOME_NOTICE_VERSION = '2026-07-30.7' +export const WELCOME_NOTICE_VERSION = '2026-08-10.1' /** The complete editable welcome notice in both supported GUI locales. */ export const WELCOME_NOTICE_COPY = { @@ -17,7 +17,7 @@ export const WELCOME_NOTICE_COPY = { paragraphs: [ '感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。', '“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。', - '为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,可以设置环境变量 DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', + '内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', ], feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们', continueLabel: '继续', @@ -27,7 +27,7 @@ export const WELCOME_NOTICE_COPY = { paragraphs: [ '感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。', '“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。', - '为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log;如需关闭,可以设置环境变量 DSH_TELEMETRY_DISABLED=1。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', + '内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', ], feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们', continueLabel: '继续', diff --git a/packages/scaffold/scripts/src/telemetry.ts b/packages/scaffold/scripts/src/telemetry.ts index d739a7164b..193cdf1fc7 100644 --- a/packages/scaffold/scripts/src/telemetry.ts +++ b/packages/scaffold/scripts/src/telemetry.ts @@ -7,9 +7,9 @@ */ import { - ConsentResolver, TelemetryReporter, buildTelemetryPayload, + resolveTelemetryConsent, type ConsentDecision, } from '@deepseek-ai/dsh-telemetry' @@ -17,7 +17,7 @@ import { export interface CommandTelemetryEvent { /** The dsh-sdk command that ran. */ command: string - /** Project directory whose consent, `cordis.yml`, and `package.json` are read. */ + /** Project directory whose `cordis.yml` and `package.json` may be reported. */ cwd: string /** Wall-clock duration in milliseconds. */ durationMs: number @@ -27,12 +27,12 @@ export interface CommandTelemetryEvent { /** Injectable consent and delivery hooks for tests. */ export interface CommandTelemetryDeps { - resolve?: (cwd: string) => Promise + resolve?: () => ConsentDecision | Promise reporter?: Pick } /** - * Resolve consent for the project and, when allowed, assemble and send one + * Resolve the shared telemetry mode and, when allowed, assemble and send one * telemetry event, draining in-flight sends before returning. Swallows every * error so telemetry can never change a command's result. * @param event - the command lifecycle facts. @@ -43,9 +43,8 @@ export async function reportCommandTelemetry( deps: CommandTelemetryDeps = {}, ): Promise { try { - /* v8 ignore next -- the production ConsentResolver is exercised by the built-bin smoke */ - const resolve = deps.resolve ?? (cwd => new ConsentResolver().resolve(cwd)) - const consent = await resolve(event.cwd) + /* v8 ignore next -- the production resolver is exercised by its owning tests */ + const consent = await (deps.resolve?.() ?? resolveTelemetryConsent()) if (!consent.allowed) return const payload = await buildTelemetryPayload({ command: event.command, diff --git a/packages/scaffold/scripts/tests/scripts.spec.ts b/packages/scaffold/scripts/tests/scripts.spec.ts index 6d2ea27991..d1f6cf81d1 100644 --- a/packages/scaffold/scripts/tests/scripts.spec.ts +++ b/packages/scaffold/scripts/tests/scripts.spec.ts @@ -611,12 +611,12 @@ describe('command telemetry', () => { const reporter = { report: () => { sent.push(1) }, flush: async () => {} } await reportCommandTelemetry( { command: 'build', cwd: dir, durationMs: 5, success: true }, - { resolve: async () => ({ allowed: true, reason: 'absent' }), reporter }, + { resolve: async () => ({ allowed: true, reason: 'FULL' }), reporter }, ) expect(sent).toHaveLength(1) await reportCommandTelemetry( { command: 'build', cwd: dir, durationMs: 5, success: true }, - { resolve: async () => ({ allowed: false, reason: 'disabled' }), reporter }, + { resolve: async () => ({ allowed: false, reason: 'DISABLED' }), reporter }, ) expect(sent).toHaveLength(1) await expect(reportCommandTelemetry( diff --git a/packages/scaffold/telemetry/README.i18n.yaml b/packages/scaffold/telemetry/README.i18n.yaml index beb145cfc0..c44209d617 100644 --- a/packages/scaffold/telemetry/README.i18n.yaml +++ b/packages/scaffold/telemetry/README.i18n.yaml @@ -2,5 +2,5 @@ # 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/scaffold/telemetry/README.md -README.md: 024ff8724604426d3d77609929ca40341f98e0fb -README.zh.md: 8d69d9289ed9c412e4d38fe9b24bb3222f81f94a +README.md: 670937d70dd2cabc0c77306a6239caa23123ae6f +README.zh.md: 6bbd17a46fe9828b3722c5703c1484eafebb8540 diff --git a/packages/scaffold/telemetry/README.md b/packages/scaffold/telemetry/README.md index 024ff87246..670937d70d 100644 --- a/packages/scaffold/telemetry/README.md +++ b/packages/scaffold/telemetry/README.md @@ -2,17 +2,17 @@ English | [中文](README.zh.md) -Launcher-side telemetry primitives for the dsh-sdk toolchain. This is a plain library the launcher imports around each command; it is **not** a Cordis plugin, because `build` and first-init `create` never boot Cordis. Wiring the reporter into the launcher command dispatch and adding the telemetry consent feature to the `dsh-helper` catalog live in their owning packages, not here. +Launcher-side telemetry primitives for the dsh-sdk toolchain. This is a plain library the launcher imports around each command; it is not a Cordis plugin because `build` and first-init `create` never boot Cordis. Wiring the reporter into launcher command dispatch lives in its owning package. | Export | Role | |---|---| | `SecretRedactor` | Conservative safety backstop: replaces secret-shaped values (secret-like keys, known token shapes, PEM blocks, URL credentials, high-entropy opaque tokens) with a placeholder in both parsed values (`redactValue`) and raw text (`redactText`). Never drops a field or line. | -| `ConsentResolver` | Parses (never boots) a project `cordis.yml` and reads the telemetry entry's enabled/disabled state as consent; `DO_NOT_TRACK`/CI env force a hard opt-out. | +| `resolveTelemetryConsent` | Reads the shared `DSH_TELEMETRY_MODE`; only `FULL` permits launcher reporting, while `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. | | `buildTelemetryPayload` | Assembles `{command, durationMs, success, cordisYmlContent, packageJsonContent}`, running the redactor over the full `cordis.yml` and `package.json` text. Never reads `.env`; `package.json` ships only alongside a `cordis.yml`, so a command run in a non-SDK directory never uploads that directory's unrelated manifest. | | `getOrCreateAnonymousId` | Random UUID persisted in the harness home resolved by [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) (`$DSH_HOME` > `~/.dsh`), scoped to that home rather than the machine, never derived from git. | | `TelemetryReporter` | Fire-and-forget send: `report()` never blocks or throws; delivery resolves on every path; `flush()` optionally drains in-flight sends within a cap. | -Consent is carried by the telemetry entry in `cordis.yml`, so disabling telemetry is disabling that entry. Telemetry reports by default and is off only when a present telemetry entry is explicitly `disabled`: a missing `cordis.yml` (first `create`), an enabled entry, or a `cordis.yml` with no telemetry entry all report. `DO_NOT_TRACK`/CI always deny. The no-config and absent-entry defaults are configurable on `ConsentResolver`. +`DSH_TELEMETRY_MODE` is the single positive consent setting for session and launcher telemetry. `FULL` enables this launcher feed; `FEEDBACK_ONLY` keeps command telemetry off and permits only feedback-triggered Session Log sharing; every other supported state keeps this feed off. The collection endpoint is a fixed constant (`DSH_TELEMETRY_ENDPOINT`). diff --git a/packages/scaffold/telemetry/README.zh.md b/packages/scaffold/telemetry/README.zh.md index 8d69d9289e..6bbd17a46f 100644 --- a/packages/scaffold/telemetry/README.zh.md +++ b/packages/scaffold/telemetry/README.zh.md @@ -2,17 +2,17 @@ [English](README.md) | 中文 -用于 dsh-sdk 工具链的启动器侧 telemetry 原语。这是启动器在执行每个命令时导入的普通库;它**不是** Cordis 插件,因为 `build` 与首次初始化的 `create` 从不启动 Cordis。将 reporter 接入启动器命令分发,并把 telemetry consent 功能加入 `dsh-helper` 目录,属于各自所属包的职责,而不是此包的职责。 +用于 dsh-sdk 工具链的启动器侧 telemetry 原语。这是启动器在执行每个命令时导入的普通库;它不是 Cordis 插件,因为 `build` 与首次初始化的 `create` 从不启动 Cordis。将 reporter 接入启动器命令分发属于其所在包的职责。 | 导出 | 职责 | |---|---| | `SecretRedactor` | 保守的安全后备:在已解析值(`redactValue`)与原始文本(`redactText`)中,将形似密钥的值(疑似密钥的键名、已知 token 格式、PEM 块、URL 凭据、高熵不透明 token)替换为占位符。绝不删除字段或行。 | -| `ConsentResolver` | 解析项目 `cordis.yml`(绝不启动),读取 telemetry 配置项的启用/禁用状态作为 consent;`DO_NOT_TRACK`/CI 环境会强制完全停止上报。 | +| `resolveTelemetryConsent` | 读取共享的 `DSH_TELEMETRY_MODE`;只有 `FULL` 允许启动器上报,`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。 | | `buildTelemetryPayload` | 组装 `{command, durationMs, success, cordisYmlContent, packageJsonContent}`,对完整的 `cordis.yml` 与 `package.json` 文本运行 redactor。绝不读取 `.env`;发送 `package.json` 的前提是同时存在 `cordis.yml`,因此在非 SDK 目录运行的命令不会上传该目录中无关的 manifest(元数据清单)。 | | `getOrCreateAnonymousId` | 将随机 UUID 持久化到 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析出的 harness home(`$DSH_HOME` > `~/.dsh`);其范围限定为该 home,而不是整台机器,且绝不从 git 派生。 | | `TelemetryReporter` | 即发即弃发送:`report()` 绝不阻塞或抛出;无论经过哪条路径,发送操作最终都会结束;`flush()` 可以在上限内排空进行中的发送。 | -Consent 由 `cordis.yml` 中的 telemetry 配置项承载,因此禁用 telemetry 就是禁用该配置项。telemetry 默认上报,只有已经存在的 telemetry 配置项被显式设为 `disabled` 时才关闭:缺少 `cordis.yml`(首次 `create`)、配置项已启用,或 `cordis.yml` 中没有 telemetry 配置项时都会上报。`DO_NOT_TRACK`/CI 始终拒绝。无配置与缺少配置项的默认值可以通过 `ConsentResolver` 配置。 +`DSH_TELEMETRY_MODE` 是会话与启动器 telemetry 的唯一正向授权配置。`FULL` 启用该启动器数据流;`FEEDBACK_ONLY` 保持命令 telemetry 关闭,只允许由反馈触发的 Session Log 共享;其他受支持的状态都会保持该数据流关闭。 收集端点是固定常量(`DSH_TELEMETRY_ENDPOINT`)。 diff --git a/packages/scaffold/telemetry/package.json b/packages/scaffold/telemetry/package.json index 6fe62e35fc..e7a5ef2ded 100644 --- a/packages/scaffold/telemetry/package.json +++ b/packages/scaffold/telemetry/package.json @@ -24,9 +24,6 @@ "lib/types/**/*.d.ts" ], "license": "BSD-3-Clause", - "dependencies": { - "yaml": "^2.9.0" - }, "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", diff --git a/packages/scaffold/telemetry/src/consent-resolver.ts b/packages/scaffold/telemetry/src/consent-resolver.ts index a4327dc9f7..190481b8ed 100644 --- a/packages/scaffold/telemetry/src/consent-resolver.ts +++ b/packages/scaffold/telemetry/src/consent-resolver.ts @@ -1,42 +1,18 @@ /** - * Consent resolution for dsh-sdk telemetry. + * Consent resolution for dsh-sdk launcher telemetry. * - * Telemetry is OFF only when `cordis.yml` contains a telemetry entry that is - * explicitly `disabled`; every other file state reports (no `cordis.yml`, an - * enabled entry, or no telemetry entry at all). The resolver PARSES `cordis.yml` - * — it never boots a Cordis application — because several launcher commands - * (`build`, `create`) never boot Cordis at all. `DO_NOT_TRACK` and CI - * environment signals force a denial regardless of file state. + * `DSH_TELEMETRY_MODE` is the shared consent setting for session and launcher + * telemetry. Only `FULL` permits launcher reporting; unset and empty values + * resolve to `DISABLED`. * * @module @deepseek-ai/dsh-telemetry/consent-resolver */ -import { readFile } from 'node:fs/promises' -import { join } from 'node:path' -import { parseDocument, type ScalarTag } from 'yaml' - -/** Default `cordis.yml` entry name that carries telemetry consent. */ -export const DEFAULT_TELEMETRY_PLUGIN_NAME = '@deepseek-ai/dsh-telemetry' - -/** - * Passthrough for Cordis' `!!js` expression tag so parsing consent never fails - * on projects that inline JavaScript expressions; the resolver only reads plain - * `name`/`disabled` scalars and does not evaluate expressions. - */ -const JS_EXPRESSION_TAG: ScalarTag = { - tag: 'tag:yaml.org,2002:js', - resolve: value => value, -} - /** Why telemetry is or is not permitted for one command. */ export type ConsentReason = - | 'enabled' - | 'disabled' - | 'absent' - | 'no-config' - | 'do-not-track' - | 'ci' - | 'unreadable' + | 'FULL' + | 'FEEDBACK_ONLY' + | 'DISABLED' /** Resolved telemetry consent for one command invocation. */ export interface ConsentDecision { @@ -46,80 +22,21 @@ export interface ConsentDecision { reason: ConsentReason } -/** Tuning for {@link ConsentResolver}; every field defaults to a documented value. */ -export interface ConsentResolverOptions { - /** `cordis.yml` entry name whose enabled state carries consent. */ - telemetryPluginName?: string - /** Environment used for `DO_NOT_TRACK`/CI checks; defaults to `process.env`. */ - env?: NodeJS.ProcessEnv - /** Honor `DO_NOT_TRACK`/CI env signals as a hard opt-out. Defaults to `true`. */ - honorEnvOptOut?: boolean - /** Consent when `cordis.yml` does not exist yet (first `create`). Defaults to `true` (telemetry is default-on). */ - allowWhenNoConfig?: boolean - /** Consent when `cordis.yml` exists but has no telemetry entry. Defaults to `true` (report unless a present entry is disabled). */ - allowWhenEntryAbsent?: boolean -} - -/** Whether an environment variable is set to a non-empty, non-"0"/"false" value. */ -function envEnabled(value: string | undefined): boolean { - if (value === undefined) return false - const normalized = value.trim().toLowerCase() - return normalized.length > 0 && normalized !== '0' && normalized !== 'false' -} - -/** Read a `cordis.yml` entry's `name`/`disabled` scalars, tolerating `!!js` tags. */ -function readTelemetryEntry(text: string, pluginName: string): { present: boolean; disabled: boolean } { - const document = parseDocument(text, { customTags: [JS_EXPRESSION_TAG] }) - const contents: unknown = document.toJS({ maxAliasCount: -1 }) - if (!Array.isArray(contents)) return { present: false, disabled: false } - for (const entry of contents) { - if (entry === null || typeof entry !== 'object') continue - const record = entry as Record - if (record.name === pluginName) return { present: true, disabled: record.disabled === true } - } - return { present: false, disabled: false } -} - -/** Resolve telemetry consent by parsing a project's `cordis.yml` and the environment. */ -export class ConsentResolver { - readonly #pluginName: string - readonly #env: NodeJS.ProcessEnv - readonly #honorEnvOptOut: boolean - readonly #allowWhenNoConfig: boolean - readonly #allowWhenEntryAbsent: boolean - - /** @param options - plugin name, environment, and default-decision knobs. */ - constructor(options: ConsentResolverOptions = {}) { - this.#pluginName = options.telemetryPluginName ?? DEFAULT_TELEMETRY_PLUGIN_NAME - this.#env = options.env ?? process.env - this.#honorEnvOptOut = options.honorEnvOptOut ?? true - this.#allowWhenNoConfig = options.allowWhenNoConfig ?? true - this.#allowWhenEntryAbsent = options.allowWhenEntryAbsent ?? true - } - - /** - * Resolve consent for a command run in the given project directory. - * @param projectDir - absolute or relative project root containing `cordis.yml`. - * @returns the consent decision and the signal that produced it. - */ - async resolve(projectDir: string): Promise { - if (this.#honorEnvOptOut) { - if (envEnabled(this.#env.DO_NOT_TRACK)) return { allowed: false, reason: 'do-not-track' } - if (envEnabled(this.#env.CI)) return { allowed: false, reason: 'ci' } - } - let text: string - try { - text = await readFile(join(projectDir, 'cordis.yml'), 'utf8') - } catch (error) { - // Missing cordis.yml is the first-init (`create`) path; any other read - // fault is treated conservatively as its own reason. - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return { allowed: this.#allowWhenNoConfig, reason: 'no-config' } - } - return { allowed: false, reason: 'unreadable' } - } - const entry = readTelemetryEntry(text, this.#pluginName) - if (!entry.present) return { allowed: this.#allowWhenEntryAbsent, reason: 'absent' } - return entry.disabled ? { allowed: false, reason: 'disabled' } : { allowed: true, reason: 'enabled' } +/** + * Resolve launcher telemetry consent from the shared telemetry mode. + * @param env - Environment containing `DSH_TELEMETRY_MODE`; defaults to `process.env`. + * @returns Whether launcher telemetry may report and the resolved mode. + */ +export function resolveTelemetryConsent(env: NodeJS.ProcessEnv = process.env): ConsentDecision { + const mode = env.DSH_TELEMETRY_MODE || 'DISABLED' + switch (mode) { + case 'FULL': + return { allowed: true, reason: 'FULL' } + case 'FEEDBACK_ONLY': + return { allowed: false, reason: 'FEEDBACK_ONLY' } + case 'DISABLED': + return { allowed: false, reason: 'DISABLED' } + default: + throw new Error(`unsupported DSH_TELEMETRY_MODE ${JSON.stringify(mode)}`) } } diff --git a/packages/scaffold/telemetry/src/index.ts b/packages/scaffold/telemetry/src/index.ts index 3c0a5beae7..97cc06178f 100644 --- a/packages/scaffold/telemetry/src/index.ts +++ b/packages/scaffold/telemetry/src/index.ts @@ -2,10 +2,9 @@ * Launcher-side telemetry for the dsh-sdk toolchain: secret redaction, consent * resolution, anonymous id, payload assembly, and a fire-and-forget reporter. * - * This package is a plain library the launcher imports around each command — it - * is NOT a Cordis plugin (several commands never boot Cordis). Wiring it into - * the launcher command dispatch and the helper feature catalog lives outside - * this package. + * This package is a plain library the launcher imports around each command; it + * is not a Cordis plugin because several commands never boot Cordis. Wiring + * reporting into launcher command dispatch lives outside this package. * * FIXME: rename to `@deepseek-ai/dsh-sdk-telemetry` before the first tagged release — * the current name collides with the `dsh-session-telemetry` family; this is @@ -24,13 +23,11 @@ export { } from './secret-redactor.ts' export type { SecretRedactorOptions } from './secret-redactor.ts' export { - ConsentResolver, - DEFAULT_TELEMETRY_PLUGIN_NAME, + resolveTelemetryConsent, } from './consent-resolver.ts' export type { ConsentDecision, ConsentReason, - ConsentResolverOptions, } from './consent-resolver.ts' export { ANONYMOUS_ID_FILE_NAME, diff --git a/packages/scaffold/telemetry/tests/consent-resolver.spec.ts b/packages/scaffold/telemetry/tests/consent-resolver.spec.ts index b9c9300e78..9a0ab4f806 100644 --- a/packages/scaffold/telemetry/tests/consent-resolver.spec.ts +++ b/packages/scaffold/telemetry/tests/consent-resolver.spec.ts @@ -1,132 +1,41 @@ -import { mkdtemp, mkdir, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { ConsentResolver, DEFAULT_TELEMETRY_PLUGIN_NAME, type ConsentDecision } from '@deepseek-ai/dsh-telemetry' +import { describe, expect, it } from 'vitest' +import { + resolveTelemetryConsent, + type ConsentDecision, +} from '@deepseek-ai/dsh-telemetry' -const dirs: string[] = [] - -async function projectDir(cordisYml?: string): Promise { - const dir = await mkdtemp(join(tmpdir(), 'dsh-consent-')) - dirs.push(dir) - if (cordisYml !== undefined) await writeFile(join(dir, 'cordis.yml'), cordisYml, 'utf8') - return dir -} - -afterEach(async () => { - await Promise.all(dirs.splice(0).map(dir => import('node:fs/promises').then(fs => fs.rm(dir, { recursive: true, force: true })))) -}) - -const enabledYml = `- id: telemetry\n name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'\n` - -describe('ConsentResolver environment opt-out', () => { - it('denies when DO_NOT_TRACK is set', async () => { - const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '1' } }).resolve(await projectDir(enabledYml)) - expect(decision).toEqual({ allowed: false, reason: 'do-not-track' }) +describe('resolveTelemetryConsent', () => { + it('denies launcher telemetry when the shared mode is unset or empty', () => { + expect(resolveTelemetryConsent({})).toEqual({ allowed: false, reason: 'DISABLED' }) + expect(resolveTelemetryConsent({ DSH_TELEMETRY_MODE: '' })) + .toEqual({ allowed: false, reason: 'DISABLED' }) }) - it('denies when CI is set', async () => { - const decision = await new ConsentResolver({ env: { CI: 'true' } }).resolve(await projectDir(enabledYml)) - expect(decision).toEqual({ allowed: false, reason: 'ci' }) + it('denies launcher telemetry in DISABLED and FEEDBACK_ONLY modes', () => { + expect(resolveTelemetryConsent({ DSH_TELEMETRY_MODE: 'DISABLED' })) + .toEqual({ allowed: false, reason: 'DISABLED' }) + expect(resolveTelemetryConsent({ DSH_TELEMETRY_MODE: 'FEEDBACK_ONLY' })) + .toEqual({ allowed: false, reason: 'FEEDBACK_ONLY' }) }) - it('ignores falsy env values and continues to the file', async () => { - const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '0', CI: 'false' } }) - .resolve(await projectDir(enabledYml)) - expect(decision).toEqual({ allowed: true, reason: 'enabled' }) + it('allows launcher telemetry only in FULL mode', () => { + expect(resolveTelemetryConsent({ DSH_TELEMETRY_MODE: 'FULL' })) + .toEqual({ allowed: true, reason: 'FULL' }) }) - it('can be told to ignore env opt-out signals', async () => { - const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '1' }, honorEnvOptOut: false }) - .resolve(await projectDir(enabledYml)) - expect(decision).toEqual({ allowed: true, reason: 'enabled' }) + it('rejects an unsupported non-empty mode', () => { + expect(() => resolveTelemetryConsent({ DSH_TELEMETRY_MODE: 'full' })) + .toThrow('unsupported DSH_TELEMETRY_MODE "full"') }) - it('reads process.env by default', async () => { - const saved = { CI: process.env.CI, DO_NOT_TRACK: process.env.DO_NOT_TRACK } - delete process.env.CI - delete process.env.DO_NOT_TRACK + it('reads process.env by default', () => { + const saved = process.env.DSH_TELEMETRY_MODE + process.env.DSH_TELEMETRY_MODE = 'FULL' try { - const decision = await new ConsentResolver().resolve(await projectDir(enabledYml)) - expect(decision).toEqual({ allowed: true, reason: 'enabled' }) + expect(resolveTelemetryConsent()).toEqual({ allowed: true, reason: 'FULL' }) } finally { - if (saved.CI !== undefined) process.env.CI = saved.CI - if (saved.DO_NOT_TRACK !== undefined) process.env.DO_NOT_TRACK = saved.DO_NOT_TRACK + if (saved === undefined) delete process.env.DSH_TELEMETRY_MODE + else process.env.DSH_TELEMETRY_MODE = saved } }) }) - -describe('ConsentResolver cordis.yml state', () => { - const resolver = new ConsentResolver({ env: {} }) - - it('allows when the telemetry entry is enabled', async () => { - expect(await resolver.resolve(await projectDir(enabledYml))) - .toEqual({ allowed: true, reason: 'enabled' }) - }) - - it('denies when the telemetry entry is disabled', async () => { - const yml = `- id: telemetry\n name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'\n disabled: true\n` - expect(await resolver.resolve(await projectDir(yml))) - .toEqual({ allowed: false, reason: 'disabled' }) - }) - - it('tolerates !!js expression tags while reading plain scalars', async () => { - const yml = [ - '- id: telemetry', - ` name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'`, - '- id: llm', - ' name: \'@deepseek-ai/dsh-llm-deepseek\'', - ' config:', - ' apiKeyEnv: DEEPSEEK_API_KEY', - ' model: !!js process.env.DEEPSEEK_MODEL', - '', - ].join('\n') - expect(await resolver.resolve(await projectDir(yml))) - .toEqual({ allowed: true, reason: 'enabled' }) - }) - - it('reports (allows) when cordis.yml has no telemetry entry', async () => { - const yml = '- id: llm\n name: \'@deepseek-ai/dsh-llm-deepseek\'\n' - expect(await resolver.resolve(await projectDir(yml))) - .toEqual({ allowed: true, reason: 'absent' }) - }) - - it('can be told to deny when the entry is absent', async () => { - const yml = '- id: llm\n name: \'@deepseek-ai/dsh-llm-deepseek\'\n' - const decision = await new ConsentResolver({ env: {}, allowWhenEntryAbsent: false }).resolve(await projectDir(yml)) - expect(decision).toEqual({ allowed: false, reason: 'absent' }) - }) - - it('skips non-object sequence items and a non-sequence root, still reporting absent', async () => { - expect(await resolver.resolve(await projectDir('- just-a-string\n- id: x\n name: y\n'))) - .toEqual({ allowed: true, reason: 'absent' }) - expect(await resolver.resolve(await projectDir('root: not-a-sequence\n'))) - .toEqual({ allowed: true, reason: 'absent' }) - }) - - it('honors a custom telemetry plugin name', async () => { - const yml = '- id: t\n name: \'my-consent-marker\'\n' - const decision = await new ConsentResolver({ env: {}, telemetryPluginName: 'my-consent-marker' }) - .resolve(await projectDir(yml)) - expect(decision).toEqual({ allowed: true, reason: 'enabled' }) - }) -}) - -describe('ConsentResolver missing or unreadable cordis.yml', () => { - it('reports no-config and allows by default on first init', async () => { - expect(await new ConsentResolver({ env: {} }).resolve(await projectDir())) - .toEqual({ allowed: true, reason: 'no-config' }) - }) - - it('can deny on first init', async () => { - const decision = await new ConsentResolver({ env: {}, allowWhenNoConfig: false }).resolve(await projectDir()) - expect(decision).toEqual({ allowed: false, reason: 'no-config' }) - }) - - it('denies with an unreadable reason when cordis.yml is not a regular file', async () => { - const dir = await projectDir() - await mkdir(join(dir, 'cordis.yml')) // a directory where the resolver expects a file - expect(await new ConsentResolver({ env: {} }).resolve(dir)) - .toEqual({ allowed: false, reason: 'unreadable' }) - }) -}) diff --git a/packages/scaffold/telemetry/tests/reporter.spec.ts b/packages/scaffold/telemetry/tests/reporter.spec.ts index 5d8a490b9e..5549251512 100644 --- a/packages/scaffold/telemetry/tests/reporter.spec.ts +++ b/packages/scaffold/telemetry/tests/reporter.spec.ts @@ -9,8 +9,8 @@ import { type TelemetryPayload, } from '@deepseek-ai/dsh-telemetry' -const ALLOW: ConsentDecision = { allowed: true, reason: 'enabled' } -const DENY: ConsentDecision = { allowed: false, reason: 'disabled' } +const ALLOW: ConsentDecision = { allowed: true, reason: 'FULL' } +const DENY: ConsentDecision = { allowed: false, reason: 'DISABLED' } const anon = (value = 'anon-123'): (() => Promise) => async () => value as AnonymousId function okResponse(): Response { diff --git a/packages/session/session-telemetry-otel/README.i18n.yaml b/packages/session/session-telemetry-otel/README.i18n.yaml index 2897eb7dac..85d6efa55a 100644 --- a/packages/session/session-telemetry-otel/README.i18n.yaml +++ b/packages/session/session-telemetry-otel/README.i18n.yaml @@ -2,5 +2,5 @@ # 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/session/session-telemetry-otel/README.md -README.md: 585995ce409255df9608bc33b76625374bc67669 -README.zh.md: 7f0b93363fbb4aebb80f0d3cc8108e58ce3f647f +README.md: 4220ed0fc27db4535ae4739d6fe784ca17518535 +README.zh.md: e0f33a7df2fcd29c79fc894b9781c8b43e2fc547 diff --git a/packages/session/session-telemetry-otel/README.md b/packages/session/session-telemetry-otel/README.md index 585995ce40..4220ed0fc2 100644 --- a/packages/session/session-telemetry-otel/README.md +++ b/packages/session/session-telemetry-otel/README.md @@ -10,7 +10,7 @@ The OpenTelemetry backend for [the telemetry seam](../session-telemetry/) — th - id: telemetry-otel name: '@deepseek-ai/dsh-session-telemetry-otel' config: - mode: FULL # FULL (default), FEEDBACK_ONLY, or DISABLED + mode: FULL # explicit opt-in; default: DISABLED shutdownTimeoutMillis: 3000 # optional; defaults to 3000 exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter url: https://collector.example.com/v1/logs @@ -21,9 +21,9 @@ The OpenTelemetry backend for [the telemetry seam](../session-telemetry/) — th | `mode` | Behavior | |---|---| -| `FULL` | Default. Each projected record, including lifecycle ops records, is handed to the OTel SDK immediately. | +| `FULL` | Each projected record, including lifecycle ops records, is handed to the OTel SDK immediately. | | `FEEDBACK_ONLY` | Each `feedback/record` replays, projects, and redacts the canonical session-log suffix through that event. Later records wait for another feedback event and remain local if none arrives. | -| `DISABLED` | No coordinator, provider, processor, or exporter is constructed. No telemetry record leaves the process. A `feedback/record` logs `session telemetry is DISABLED; nothing will be shared and this feedback remains local`; the event remains in the local session log. | +| `DISABLED` | Default. No coordinator, provider, processor, or exporter is constructed. No telemetry record leaves the process. A `feedback/record` logs `session telemetry is DISABLED; nothing will be shared and this feedback remains local`; the event remains in the local session log. | Programmatic TypeScript configuration uses the exported `TelemetryMode` enum (`TelemetryMode.FULL`, `TelemetryMode.FEEDBACK_ONLY`, or `TelemetryMode.DISABLED`); raw string literals are not assignable. Serialized Cordis configuration continues to use the string values shown above. diff --git a/packages/session/session-telemetry-otel/README.zh.md b/packages/session/session-telemetry-otel/README.zh.md index 7f0b93363f..e0f33a7df2 100644 --- a/packages/session/session-telemetry-otel/README.zh.md +++ b/packages/session/session-telemetry-otel/README.zh.md @@ -10,7 +10,7 @@ - id: telemetry-otel name: '@deepseek-ai/dsh-session-telemetry-otel' config: - mode: FULL # FULL (default), FEEDBACK_ONLY, or DISABLED + mode: FULL # explicit opt-in; default: DISABLED shutdownTimeoutMillis: 3000 # optional; defaults to 3000 exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter url: https://collector.example.com/v1/logs @@ -21,9 +21,9 @@ | `mode` | 行为 | |---|---| -| `FULL` | 默认值。每条已投影记录都立即交给 OTel SDK,包括生命周期运维记录。 | +| `FULL` | 每条已投影记录都立即交给 OTel SDK,包括生命周期运维记录。 | | `FEEDBACK_ONLY` | 每个 `feedback/record` 都会回放权威会话日志中截至该事件的后缀,并进行投影与脱敏。后续记录等待下一个反馈事件;如果没有后续反馈,则留在本地。 | -| `DISABLED` | 不构造协调器、提供方、处理器或导出器。没有遥测记录会离开进程。`feedback/record` 会记录 `session telemetry is DISABLED; nothing will be shared and this feedback remains local`;该事件留在本地会话日志中。 | +| `DISABLED` | 默认值。不构造协调器、提供方、处理器或导出器。没有遥测记录会离开进程。`feedback/record` 会记录 `session telemetry is DISABLED; nothing will be shared and this feedback remains local`;该事件留在本地会话日志中。 | 程序化 TypeScript 配置使用导出的 `TelemetryMode` 枚举(`TelemetryMode.FULL`、`TelemetryMode.FEEDBACK_ONLY` 或 `TelemetryMode.DISABLED`);原始字符串字面量不可赋值。序列化后的 Cordis 配置继续使用上表所示的字符串值。 diff --git a/packages/session/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts index 50776f7d3f..d16e8a4d2d 100644 --- a/packages/session/session-telemetry-otel/src/index.ts +++ b/packages/session/session-telemetry-otel/src/index.ts @@ -47,7 +47,7 @@ export enum TelemetryMode { } /** Default session-sharing policy for schema and direct construction. */ -export const DEFAULT_TELEMETRY_MODE = TelemetryMode.FULL +export const DEFAULT_TELEMETRY_MODE = TelemetryMode.DISABLED const DISABLED_FEEDBACK_WARNING = 'session telemetry is DISABLED; nothing will be shared and this feedback remains local' const NON_CANONICAL_FEEDBACK_WARNING = 'session telemetry ignored a feedback event absent from the canonical session log' @@ -77,7 +77,7 @@ function assertNever(value: never): never { * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { - /** Sharing policy; defaults to immediate `FULL` delivery. */ + /** Sharing policy; defaults to local-only `DISABLED` behavior. */ mode?: TelemetryMode /** * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete diff --git a/packages/session/session-telemetry-otel/tests/otel.spec.ts b/packages/session/session-telemetry-otel/tests/otel.spec.ts index 511c95c0d8..a3d7b7c25d 100644 --- a/packages/session/session-telemetry-otel/tests/otel.spec.ts +++ b/packages/session/session-telemetry-otel/tests/otel.spec.ts @@ -99,6 +99,7 @@ async function boot(url: string) { const ctx = new Context() await ctx.plugin(SessionStore) const fiber = await ctx.plugin(TelemetryOtel, { + mode: TelemetryMode.FULL, exporter: { url, headers: { authorization: 'Bearer test-token' } }, }) return { ctx, fiber } @@ -180,6 +181,7 @@ describe('TelemetryOtel wire', () => { const ctx = new Context() await ctx.plugin(SessionStore) const fiber = await ctx.plugin(TelemetryOtel, { + mode: TelemetryMode.FULL, exporter: { url }, processor: { scheduledDelayMillis: 10 }, }) @@ -210,6 +212,7 @@ describe('TelemetryOtel wire', () => { const ctx = new Context() await ctx.plugin(SessionStore) const fiber = await ctx.plugin(TelemetryOtel, { + mode: TelemetryMode.FULL, exporter: { url, timeoutMillis: 60_000 }, processor: { scheduledDelayMillis: 10, exportTimeoutMillis: 60_000 }, shutdownTimeoutMillis: 50, @@ -238,6 +241,7 @@ describe('TelemetryOtel wire', () => { // verbatim passthrough must hand it (and every other field) to the // exporter rather than silently rebuilding url/headers only. const fiber = await ctx.plugin(TelemetryOtel, { + mode: TelemetryMode.FULL, exporter: { url, compression: 'gzip' }, } as Config) const session = ctx.sessions.create(SessionId('gzip'), { meta: {} }) @@ -364,16 +368,24 @@ describe('TelemetryOtel wire', () => { expect(captures).toEqual([]) }) - it('defaults direct construction to full delivery', async () => { + it('defaults direct construction to disabled delivery', async () => { const { url, captures } = await mockCollector() const ctx = new Context() await ctx.plugin(SessionStore) - new TelemetryOtel(ctx, { exporter: { url } }) + const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) + new TelemetryOtel(ctx, { + exporter: { url }, + processor: { maxExportBatchSize: 0 }, + }) const session = ctx.sessions.create(SessionId('direct-default'), { meta: {} }) session.append('turn/start', { turn: 1 }) + recordFeedback(session, 'local report') await ctx.fiber.dispose() - expect(eventTypes(captures)).toContain('turn/start') + expect(warn).toHaveBeenCalledWith( + 'session telemetry is DISABLED; nothing will be shared and this feedback remains local', + ) + expect(captures).toEqual([]) }) }) @@ -382,23 +394,23 @@ describe('TelemetryOtel config fails loud', () => { expectTypeOf().toEqualTypeOf() expectTypeOf<'FULL'>().not.toExtend() expectTypeOf().toExtend() - expect(DEFAULT_TELEMETRY_MODE).toBe(TelemetryMode.FULL) + expect(DEFAULT_TELEMETRY_MODE).toBe(TelemetryMode.DISABLED) expect(Config({}).mode).toBe(DEFAULT_TELEMETRY_MODE) }) it.each([ - [{}, /exporter\.url is required/], - [{ exporter: { url: '' } }, /exporter\.url is required/], - [{ exporter: { url: 'not a url' } }, /not a valid URL/], - [{ exporter: { url: 'ftp://collector' } }, /must be http\(s\)/], + [{ mode: TelemetryMode.FULL }, /exporter\.url is required/], + [{ mode: TelemetryMode.FULL, exporter: { url: '' } }, /exporter\.url is required/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'not a url' } }, /not a valid URL/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'ftp://collector' } }, /must be http\(s\)/], [{ mode: TelemetryMode.FEEDBACK_ONLY }, /exporter\.url is required/], [{ mode: 'INVALID' }, /INVALID/], // The SDK accepts a non-positive batch size but its shutdown drain then // splices empty batches forever — dispose would hang, so reject at load. - [{ exporter: { url: 'http://c/v1/logs' }, processor: { maxExportBatchSize: 0 } }, /maxExportBatchSize/], - [{ exporter: { url: 'http://c/v1/logs' }, processor: { maxExportBatchSize: 0.5 } }, /maxExportBatchSize/], - [{ exporter: { url: 'http://c/v1/logs' }, shutdownTimeoutMillis: 0 }, /shutdownTimeoutMillis/], - [{ exporter: { url: 'http://c/v1/logs' }, shutdownTimeoutMillis: Number.POSITIVE_INFINITY }, /shutdownTimeoutMillis/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'http://c/v1/logs' }, processor: { maxExportBatchSize: 0 } }, /maxExportBatchSize/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'http://c/v1/logs' }, processor: { maxExportBatchSize: 0.5 } }, /maxExportBatchSize/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'http://c/v1/logs' }, shutdownTimeoutMillis: 0 }, /shutdownTimeoutMillis/], + [{ mode: TelemetryMode.FULL, exporter: { url: 'http://c/v1/logs' }, shutdownTimeoutMillis: Number.POSITIVE_INFINITY }, /shutdownTimeoutMillis/], ])('rejects %j at plugin load', async (config, message) => { const ctx = new Context() await ctx.plugin(SessionStore) @@ -463,7 +475,7 @@ describe('dsh-session-telemetry-otel real-load-path guard', () => { const unwrapped = loader.unwrapExports(module) as Parameters[0] const ctx = new Context() await ctx.plugin(SessionStore) - const fiber = await ctx.plugin(unwrapped, { exporter: { url } }) + const fiber = await ctx.plugin(unwrapped, { mode: TelemetryMode.FULL, exporter: { url } }) expect(ctx.telemetry).toBeInstanceOf(TelemetryOtel) await fiber.dispose() }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 732e00e4ee..c41ecab4a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5493,10 +5493,6 @@ importers: version: link:../../../vendor/cordis packages/scaffold/telemetry: - dependencies: - yaml: - specifier: ^2.9.0 - version: 2.9.0 devDependencies: '@deepseek-ai/dsh-brand': specifier: workspace:^ diff --git a/scripts/snapshots/translation-prompt-v4/request-response.expected.json b/scripts/snapshots/translation-prompt-v4/request-response.expected.json index 63050b2079..63a1243f2b 100644 --- a/scripts/snapshots/translation-prompt-v4/request-response.expected.json +++ b/scripts/snapshots/translation-prompt-v4/request-response.expected.json @@ -8,11 +8,11 @@ }, { "role": "user", - "content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" + "content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nSession Logs stay local by default. Set `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` to share a Session Log only when submitting feedback, or `DSH_TELEMETRY_MODE=FULL` to upload continuously. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow DeepSeek Harness on Twitter for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n" }, { "role": "assistant", - "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" + "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\nSession Log 默认留在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 可仅在提交反馈时共享 Session Log,设置 `DSH_TELEMETRY_MODE=FULL` 可持续上传。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone \ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACP(Agent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

\n \"DeepSeek\n

\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n" }, { "role": "user",