From b34b967e73c87fda951b95e441459bd0fa0a686d Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 21 Oct 2026 18:19:41 +0800 Subject: [PATCH 1/2] feat(telemetry): attribute agent vs human callers via BAILIAN_CALLER_* env --- docs/agents/skill-change.md | 1 + docs/agents/telemetry-change.md | 37 +++-- packages/core/src/telemetry/caller.ts | 59 ++++++++ packages/core/src/telemetry/event.ts | 14 ++ packages/core/src/telemetry/index.ts | 3 + packages/core/src/telemetry/tracker.ts | 4 + packages/core/tests/telemetry-caller.test.ts | 142 ++++++++++++++++++ packages/core/tests/telemetry-tracker.test.ts | 80 +++++++++- packages/runtime/src/middleware.ts | 2 + .../runtime/tests/telemetry-stage.test.ts | 87 +++++++++++ skills/bailian-cli/SKILL.md | 2 +- skills/bailian-finetune/SKILL.md | 2 +- skills/bailian-gen/SKILL.md | 2 +- skills/bailian-managed-agent/SKILL.md | 2 +- skills/bailian-memory/SKILL.md | 2 +- skills/bailian-protocol/SKILL.md | 35 ++++- skills/bailian-protocol/assets/setup.md | 2 + skills/bailian-protocol/assets/versioning.md | 9 +- skills/bailian-sandbox/SKILL.md | 2 +- skills/bailian-web-search/SKILL.md | 2 +- 20 files changed, 463 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/telemetry/caller.ts create mode 100644 packages/core/tests/telemetry-caller.test.ts create mode 100644 packages/runtime/tests/telemetry-stage.test.ts diff --git a/docs/agents/skill-change.md b/docs/agents/skill-change.md index 4136f17ed..225332391 100644 --- a/docs/agents/skill-change.md +++ b/docs/agents/skill-change.md @@ -40,6 +40,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search - [ ] **整包装齐**:安装 / 缺 skill 文案主推 `bl skill init`;升级同步用 `bl skill update`(只更新已安装,不装新 skill);业务 skill **不**声明 `companions` - [ ] **协议读取**:CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init` +- [ ] **调用归因**:业务 skill 与协议预检执行 `bl` 时必须带 Agent 归因前缀(见 `bailian-protocol`「Call attribution」);人类终端不要设这些变量 - [ ] **高风险确认**:统一由 `bailian-protocol` 定义;reference / leaf help 以 `risk: high` 明示风险,业务 skill 不得引导 Agent 自动补 `--yes`。遇到 exit code 7 / `requires_confirmation` 时停止执行并请求确认;目标或范围变化后重新确认 - [ ] **正常控制流**:`requires_confirmation` 不是 CLI bug,`assets/issue-reporting.md` 必须将 exit code 7 保持在 EXCLUDE 范围 - [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read,未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提 diff --git a/docs/agents/telemetry-change.md b/docs/agents/telemetry-change.md index 8dc0e68fb..d8e6ed9f0 100644 --- a/docs/agents/telemetry-change.md +++ b/docs/agents/telemetry-change.md @@ -95,9 +95,18 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网 - `command`、`timestamp`、`durationMs`、`success` - `cliVersion`、`nodeVersion`、`os` - `authMethod` +- `callerType`(必填:`agent` 或 `human`);`callerType=agent` 时可选 `callerName`、`skillName`、`skillVersion` - 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId` - 安全 allowlist 过滤后的 `params` +调用方归因只认本次进程的 `BAILIAN_CALLER_*`(由 `readCaller` 归一化后写入事件),口径对齐 Ark CLI: + +- `BAILIAN_CALLER_TYPE` 为 `agent` / `ai_agent`(大小写不敏感)→ `callerType=agent` +- 未设置、空白、显式 `human` 或其他值 → `callerType=human`;人类终端不要手动设这些变量 +- `callerName` / `skillName` / `skillVersion` 仅在 `agent` 时发送;`kscli` 通常没有 skill 字段 + +新 CLI 每条进入 middleware 的命令事件都有 `callerType`。旧 CLI 没有该字段。看板只统计带 `callerType` 的事件,不能把字段缺失混进分母。新 CLI + 旧 skill / Agent 漏设变量时,会落进 `human`(与 Ark 相同,靠 skill 强制贴标签收敛)。`BAILIAN_CALLER_*` 是调用方声明,不是 `Identity`,不得用来猜 `bl` / `kscli`。 + 参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。 `page` 仅记录有限数值页码;Webhook 等命令使用的不透明字符串游标不得进入本地日志或远端 AEM,即使字符串内容看起来是数字。 @@ -113,16 +122,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网 AEM 映射: -| AEM 字段 | 内容 | -| ---------- | ------------------------------------------------------------------ | -| event name | 命令路径 | -| `et` | `EXP` | -| `ext` | 除 `command`、`params` 外的结构化事件字段,包含失败时的 `exitCode` | -| `c1` | allowlist 参数 | -| `c2` | `success` / `failure` | -| `c3` | HTTP status | -| `c4` | 错误文案,最多 500 字符 | -| `c5` | request ID | +| AEM 字段 | 内容 | +| ---------- | -------------------------------------------------------------------------------- | +| event name | 命令路径 | +| `et` | `EXP` | +| `ext` | 除 `command`、`params` 外的结构化事件字段,含 `callerType` 与失败时的 `exitCode` | +| `c1` | allowlist 参数 | +| `c2` | `success` / `failure` | +| `c3` | HTTP status | +| `c4` | 错误文案,最多 500 字符 | +| `c5` | request ID | 远端发送是 best-effort,不得阻塞命令或改变退出码。正常退出最多等待 1 秒,SIGINT 最多等待 500 ms。 @@ -140,6 +149,7 @@ AEM 映射: - [ ] 同时核对 `packages/core/src/client/http.ts`、`mcp.ts`、`instrumented-fetch.ts`、`client.ts` 与 `files/upload.ts` - [ ] 产品身份必须来自 `Identity`;不得从命令路径、环境变量或 `process.argv` 猜测 +- [ ] `BAILIAN_CALLER_*` 是调用方声明,不得用来猜产品身份(`bl` / `kscli`) - [ ] `bl` 与 `kscli` 必须分别验证 `binName`、`clientName`、`version` - [ ] OSS、结果文件、npm、二进制和 Skill 下载不得为了业务渠道统计新增 source-config - [ ] 改 URL / host 范围时同时执行 [URL / 渠道变更](url-change.md) 清单 @@ -148,6 +158,7 @@ AEM 映射: - [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射 - [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径 +- [ ] 调用方字段经 `readCaller` → `TrackingDeps.caller` → 事件;未设置默认 `human`,仅显式 Agent 声明为 `agent` - [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码 - [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5` - [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口 @@ -157,15 +168,17 @@ AEM 映射: ```sh rg -n "trackingHeaders|x-dashscope-source-config|User-Agent" packages --glob '*.ts' -rg -n "trackCommandExecution|PARAM_ALLOWLIST|buildRemoteAemOptions" packages/core packages/runtime --glob '*.ts' +rg -n "trackCommandExecution|PARAM_ALLOWLIST|buildRemoteAemOptions|readCaller" packages/core packages/runtime --glob '*.ts' vp check -vp test packages/core/tests packages/commands/tests/e2e/auth.e2e.test.ts +vp test packages/core/tests packages/runtime/tests/telemetry-stage.test.ts packages/commands/tests/e2e/auth.e2e.test.ts ``` ## 常见漏点 - ✗ 只看 AEM 命令事件,误以为它能替代网关侧请求渠道统计 - ✗ 把 `authMethod` 当成实际凭证来源;它只是命令声明的鉴权域 +- ✗ 把旧 CLI「没有 `callerType`」和「`callerType=human`」混进同一分母宣称人工占比 +- ✗ 用 `BAILIAN_CALLER_*` 猜测产品身份,或根据 TTY / 安装目录反推调用方 - ✗ 新增 bypass `fetch` 后漏掉应由网关消费的 source-config,或把它发给 OSS / npm / 第三方下载地址 - ✗ 只改 `bl` 入口,导致 `kscli` 的产品名或版本标签错误 - ✗ 把帮助、版本或参数校验失败算进“全部命令”;这些路径当前没有进入 telemetry middleware diff --git a/packages/core/src/telemetry/caller.ts b/packages/core/src/telemetry/caller.ts new file mode 100644 index 000000000..6c1b1df30 --- /dev/null +++ b/packages/core/src/telemetry/caller.ts @@ -0,0 +1,59 @@ +/** Parse BAILIAN_CALLER_* from the process env. Defaults to human. */ + +export type CallerAttribution = + | { callerType: "human" } + | { + callerType: "agent"; + callerName: string; + skillName?: string; + skillVersion?: string; + }; + +const CALLER_TYPE_ENV = "BAILIAN_CALLER_TYPE"; +const CALLER_NAME_ENV = "BAILIAN_CALLER_NAME"; +const SKILL_NAME_ENV = "BAILIAN_SKILL_NAME"; +const SKILL_VERSION_ENV = "BAILIAN_SKILL_VERSION"; + +const NAME_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/; +const SKILL_NAME_PATTERN = /^bailian-[a-z0-9-]{1,64}$/; +const SKILL_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]{1,20})?$/; + +function readTrimmed(env: NodeJS.ProcessEnv, key: string): string | undefined { + const raw = env[key]; + if (typeof raw !== "string") return undefined; + const trimmed = raw.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +/** Normalize host-reported name to a stable id, or `unknown`. */ +function normalizeCallerName(raw: string | undefined): string { + if (!raw) return "unknown"; + const normalized = raw.toLowerCase().replace(/[\s_]+/g, "-"); + return NAME_PATTERN.test(normalized) ? normalized : "unknown"; +} + +/** Env-only attribution; no TTY or install-path heuristics. */ +export function readCaller(env: NodeJS.ProcessEnv): CallerAttribution { + const callerType = readTrimmed(env, CALLER_TYPE_ENV)?.toLowerCase(); + if (callerType !== "agent" && callerType !== "ai_agent") { + return { callerType: "human" }; + } + + const skillNameRaw = readTrimmed(env, SKILL_NAME_ENV)?.toLowerCase(); + const skillName = + skillNameRaw !== undefined && SKILL_NAME_PATTERN.test(skillNameRaw) ? skillNameRaw : undefined; + const skillVersionRaw = readTrimmed(env, SKILL_VERSION_ENV); + const skillVersion = + skillName !== undefined && + skillVersionRaw !== undefined && + SKILL_VERSION_PATTERN.test(skillVersionRaw) + ? skillVersionRaw + : undefined; + + return { + callerType: "agent", + callerName: normalizeCallerName(readTrimmed(env, CALLER_NAME_ENV)), + ...(skillName !== undefined ? { skillName } : {}), + ...(skillVersion !== undefined ? { skillVersion } : {}), + }; +} diff --git a/packages/core/src/telemetry/event.ts b/packages/core/src/telemetry/event.ts index 228bf699f..911ba2b63 100644 --- a/packages/core/src/telemetry/event.ts +++ b/packages/core/src/telemetry/event.ts @@ -1,4 +1,5 @@ import type { AuthRequirement } from "../types/command.ts"; +import type { CallerAttribution } from "./caller.ts"; export interface TrackingEvent { command: string; @@ -14,6 +15,11 @@ export interface TrackingEvent { os: string; authMethod?: AuthRequirement; params?: Record; + /** Always present on new CLI events: `agent` or `human`. */ + callerType: CallerAttribution["callerType"]; + callerName?: string; + skillName?: string; + skillVersion?: string; } export function createTrackingEvent(opts: { @@ -24,6 +30,7 @@ export function createTrackingEvent(opts: { cliVersion: string; authMethod?: AuthRequirement; params?: Record; + caller: CallerAttribution; }): TrackingEvent { const event: TrackingEvent = { command: opts.command, @@ -33,8 +40,15 @@ export function createTrackingEvent(opts: { cliVersion: opts.cliVersion, nodeVersion: process.version, os: process.platform, + callerType: opts.caller.callerType, }; + if (opts.caller.callerType === "agent") { + event.callerName = opts.caller.callerName; + if (opts.caller.skillName !== undefined) event.skillName = opts.caller.skillName; + if (opts.caller.skillVersion !== undefined) event.skillVersion = opts.caller.skillVersion; + } + if (opts.authMethod) { event.authMethod = opts.authMethod; } diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts index a21f78571..e70ea5fb4 100644 --- a/packages/core/src/telemetry/index.ts +++ b/packages/core/src/telemetry/index.ts @@ -1,4 +1,7 @@ +export type { CallerAttribution } from "./caller.ts"; +export { readCaller } from "./caller.ts"; export type { TrackingEvent } from "./event.ts"; export { createTrackingEvent } from "./event.ts"; export { localSink, remoteSink, flushTelemetry } from "./sink.ts"; export { trackCommandExecution } from "./tracker.ts"; +export type { TrackingDeps } from "./tracker.ts"; diff --git a/packages/core/src/telemetry/tracker.ts b/packages/core/src/telemetry/tracker.ts index a65534835..2f161016e 100644 --- a/packages/core/src/telemetry/tracker.ts +++ b/packages/core/src/telemetry/tracker.ts @@ -1,6 +1,7 @@ import type { Identity, Settings } from "../config/schema.ts"; import { BailianError } from "../errors/base.ts"; import type { AuthRequirement } from "../types/command.ts"; +import type { CallerAttribution } from "./caller.ts"; import { createTrackingEvent } from "./event.ts"; import { localSink, remoteSink } from "./sink.ts"; @@ -89,6 +90,8 @@ export interface TrackingDeps { identity: Identity; settings: Settings; authMethod?: AuthRequirement; + /** From `readCaller(process.env)` in telemetryStage. */ + caller: CallerAttribution; } export async function trackCommandExecution( @@ -133,6 +136,7 @@ export async function trackCommandExecution( cliVersion: deps.identity.version, authMethod: deps.authMethod, params: extractParams(flags), + caller: deps.caller, }); // Fire-and-forget — never block the CLI exit or mask errors diff --git a/packages/core/tests/telemetry-caller.test.ts b/packages/core/tests/telemetry-caller.test.ts new file mode 100644 index 000000000..057a578af --- /dev/null +++ b/packages/core/tests/telemetry-caller.test.ts @@ -0,0 +1,142 @@ +import { expect, test } from "vite-plus/test"; +import { readCaller } from "../src/telemetry/caller.ts"; + +test.each([ + { label: "missing", env: {} }, + { label: "blank", env: { BAILIAN_CALLER_TYPE: " " } }, + { label: "explicit human", env: { BAILIAN_CALLER_TYPE: "human" } }, + { label: "illegal false", env: { BAILIAN_CALLER_TYPE: "false" } }, + { label: "illegal ai-agent", env: { BAILIAN_CALLER_TYPE: "ai-agent" } }, + { label: "illegal zero", env: { BAILIAN_CALLER_TYPE: "0" } }, +])("maps $label to human without agent fields", ({ env }) => { + expect(readCaller(env)).toEqual({ callerType: "human" }); +}); + +test.each([ + { label: "agent", type: "agent" }, + { label: "ai_agent", type: "ai_agent" }, + { label: "Agent", type: "Agent" }, + { label: "AI_AGENT", type: "AI_AGENT" }, + { label: "padded agent", type: " agent " }, +])("accepts $label as agent", ({ type }) => { + expect( + readCaller({ + BAILIAN_CALLER_TYPE: type, + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "bailian-cli", + BAILIAN_SKILL_VERSION: "2.5.0", + }), + ).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", + }); +}); + +test("normalizes callerName and falls back to unknown", () => { + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: " Claude Code ", + }), + ).toEqual({ callerType: "agent", callerName: "claude-code" }); + + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "qwen_code", + }), + ).toEqual({ callerType: "agent", callerName: "qwen-code" }); + + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "bad/name!", + }), + ).toEqual({ callerType: "agent", callerName: "unknown" }); + + expect(readCaller({ BAILIAN_CALLER_TYPE: "agent" })).toEqual({ + callerType: "agent", + callerName: "unknown", + }); +}); + +test("drops illegal skillName and only keeps version when skillName is valid", () => { + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "not-bailian", + BAILIAN_SKILL_VERSION: "2.5.0", + }), + ).toEqual({ callerType: "agent", callerName: "cursor" }); + + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "bailian-cli", + BAILIAN_SKILL_VERSION: "not-a-version", + }), + ).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + }); + + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "bailian-cli", + BAILIAN_SKILL_VERSION: "2.5.0-beta.1", + }), + ).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0-beta.1", + }); +}); + +test("lowercases skillName before validation", () => { + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "Bailian-CLI", + BAILIAN_SKILL_VERSION: "2.5.0", + }), + ).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", + }); + + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "agent", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "BAILIAN-GEN", + BAILIAN_SKILL_VERSION: "2.5.0", + }), + ).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-gen", + skillVersion: "2.5.0", + }); +}); + +test("ignores agent detail fields when callerType is human", () => { + expect( + readCaller({ + BAILIAN_CALLER_TYPE: "human", + BAILIAN_CALLER_NAME: "cursor", + BAILIAN_SKILL_NAME: "bailian-cli", + BAILIAN_SKILL_VERSION: "2.5.0", + }), + ).toEqual({ callerType: "human" }); +}); diff --git a/packages/core/tests/telemetry-tracker.test.ts b/packages/core/tests/telemetry-tracker.test.ts index 1870673ae..c623806a6 100644 --- a/packages/core/tests/telemetry-tracker.test.ts +++ b/packages/core/tests/telemetry-tracker.test.ts @@ -2,6 +2,7 @@ import { expect, test, vi } from "vite-plus/test"; import type { Identity, Settings } from "../src/config/schema.ts"; import { BailianError } from "../src/errors/base.ts"; import { ExitCode } from "../src/errors/codes.ts"; +import type { CallerAttribution } from "../src/telemetry/caller.ts"; import { buildRemoteAemOptions, type TrackingEvent } from "../src/telemetry/event.ts"; const sinkMocks = vi.hoisted(() => ({ @@ -20,6 +21,14 @@ const identity: Identity = { npmPackage: "bailian-cli", }; +const humanCaller: CallerAttribution = { callerType: "human" }; +const agentCaller: CallerAttribution = { + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", +}; + test("records BailianError exitCode in local events and AEM ext", async () => { sinkMocks.localSink.mockClear(); sinkMocks.remoteSink.mockClear(); @@ -31,6 +40,7 @@ test("records BailianError exitCode in local events and AEM ext", async () => { identity, settings: { telemetry: true } as Settings, authMethod: "apiKey", + caller: humanCaller, }, ["knowledge", "doc", "delete"], {}, @@ -47,9 +57,13 @@ test("records BailianError exitCode in local events and AEM ext", async () => { success: false, exitCode: 7, errorMessage: "该操作会永久删除文档。", + callerType: "human", }); + expect(event.callerName).toBeUndefined(); + expect(event.skillName).toBeUndefined(); + expect(event.skillVersion).toBeUndefined(); expect(buildRemoteAemOptions(event)).toMatchObject({ - ext: expect.objectContaining({ exitCode: 7 }), + ext: expect.objectContaining({ exitCode: 7, callerType: "human" }), }); }); @@ -67,7 +81,12 @@ test.each([ sinkMocks.remoteSink.mockClear(); await trackCommandExecution( - { identity, settings: { telemetry: true } as Settings, authMethod: "apiKey" }, + { + identity, + settings: { telemetry: true } as Settings, + authMethod: "apiKey", + caller: humanCaller, + }, ["managed-agent", "webhook", "events"], { page, pageSize: 20 }, async () => {}, @@ -82,3 +101,60 @@ test.each([ expect(buildRemoteAemOptions(remoteEvent).c1).toEqual(expectedParams); }, ); + +test("keeps agent attribution on success and in AEM ext", async () => { + sinkMocks.localSink.mockClear(); + sinkMocks.remoteSink.mockClear(); + + await trackCommandExecution( + { + identity, + settings: { telemetry: true } as Settings, + authMethod: "apiKey", + caller: agentCaller, + }, + ["config", "show"], + {}, + async () => {}, + ); + + const event = sinkMocks.localSink.mock.calls[0]![0]; + expect(event).toMatchObject({ + success: true, + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", + }); + expect(event.params).toBeUndefined(); + expect(buildRemoteAemOptions(event)).toMatchObject({ + ext: expect.objectContaining({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", + }), + }); +}); + +test("does not call sinks when telemetry is disabled", async () => { + sinkMocks.localSink.mockClear(); + sinkMocks.remoteSink.mockClear(); + const next = vi.fn(async () => {}); + + await trackCommandExecution( + { + identity, + settings: { telemetry: false } as Settings, + authMethod: "apiKey", + caller: agentCaller, + }, + ["config", "show"], + {}, + next, + ); + + expect(next).toHaveBeenCalledOnce(); + expect(sinkMocks.localSink).not.toHaveBeenCalled(); + expect(sinkMocks.remoteSink).not.toHaveBeenCalled(); +}); diff --git a/packages/runtime/src/middleware.ts b/packages/runtime/src/middleware.ts index b44271b2a..c98e186f5 100644 --- a/packages/runtime/src/middleware.ts +++ b/packages/runtime/src/middleware.ts @@ -19,6 +19,7 @@ import { resolveApiKey, resolveConsole, resolveOpenApi, + readCaller, resolveModelBaseUrlState, selectApiKeyResolutionSources, trackCommandExecution, @@ -176,6 +177,7 @@ export const telemetryStage: Middleware = (ctx, next) => { identity: ctx.identity, settings: ctx.settings, authMethod: ctx.command.auth, + caller: readCaller(process.env), }, ctx.path, ctx.flags, diff --git a/packages/runtime/tests/telemetry-stage.test.ts b/packages/runtime/tests/telemetry-stage.test.ts new file mode 100644 index 000000000..1e3133298 --- /dev/null +++ b/packages/runtime/tests/telemetry-stage.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; +import type { CallerAttribution } from "bailian-cli-core"; + +const trackerMocks = vi.hoisted(() => ({ + trackCommandExecution: vi.fn( + async ( + _deps: unknown, + _path: string[], + _flags: Record, + next: () => Promise, + ) => { + await next(); + }, + ), +})); + +vi.mock("bailian-cli-core", async () => { + const actual = await vi.importActual("bailian-cli-core"); + return { + ...actual, + trackCommandExecution: trackerMocks.trackCommandExecution, + }; +}); + +import { telemetryStage, type RunContext } from "../src/middleware.ts"; + +function makeContext(): RunContext { + return { + identity: { + binName: "bl", + version: "0.0.0-test", + clientName: "bailian-cli-test", + npmPackage: "bailian-cli", + }, + path: ["config", "show"], + command: { auth: "none" }, + settings: { telemetry: true }, + flags: {}, + } as RunContext; +} + +beforeEach(() => { + trackerMocks.trackCommandExecution.mockClear(); +}); + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +test("wires process.env through readCaller into trackCommandExecution", async () => { + vi.stubEnv("BAILIAN_CALLER_TYPE", "agent"); + vi.stubEnv("BAILIAN_CALLER_NAME", "cursor"); + vi.stubEnv("BAILIAN_SKILL_NAME", "bailian-cli"); + vi.stubEnv("BAILIAN_SKILL_VERSION", "2.5.0"); + const next = vi.fn(async () => {}); + + await telemetryStage(makeContext(), next); + + expect(trackerMocks.trackCommandExecution).toHaveBeenCalledOnce(); + const deps = trackerMocks.trackCommandExecution.mock.calls[0]![0] as { + caller: CallerAttribution; + authMethod: string; + }; + expect(deps.caller).toEqual({ + callerType: "agent", + callerName: "cursor", + skillName: "bailian-cli", + skillVersion: "2.5.0", + }); + expect(deps.authMethod).toBe("none"); + expect(next).toHaveBeenCalledOnce(); +}); + +test("defaults to human when caller env is unset", async () => { + vi.stubEnv("BAILIAN_CALLER_TYPE", undefined); + vi.stubEnv("BAILIAN_CALLER_NAME", undefined); + vi.stubEnv("BAILIAN_SKILL_NAME", undefined); + vi.stubEnv("BAILIAN_SKILL_VERSION", undefined); + const next = vi.fn(async () => {}); + + await telemetryStage(makeContext(), next); + + const deps = trackerMocks.trackCommandExecution.mock.calls[0]![0] as { + caller: CallerAttribution; + }; + expect(deps.caller).toEqual({ callerType: "human" }); +}); diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 24a78594a..7d1e36d9b 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -21,7 +21,7 @@ description: >- # Aliyun Model Studio CLI (`bl`) -**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent, Version & updates (pre-flight checklist), Setup & auth, and CLI errors: report an issue. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** +**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent, Version & updates (pre-flight checklist), Call attribution (every Agent `bl` command), Setup & auth, and CLI errors: report an issue. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** > **Family hub** — This skill owns Bailian resource commands and the hub `reference/` (apps, knowledge, usage, auth, config, …). > Shared protocol → [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md) (install the full family with `bl skill init`). diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index c00c1a514..1d7b4e30e 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -16,7 +16,7 @@ description: >- # Bailian fine-tuning pipeline (`bl dataset` / `bl finetune` / `bl deploy`) -**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Version & updates (pre-flight checklist), Setup & auth, and CLI errors: report an issue. Command details are authoritative in [`reference/`](reference/index.md) (dataset / finetune / deploy) and `bl --help` — do not guess flags. The whole pipeline requires an API key. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** +**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Version & updates (pre-flight checklist), Call attribution (every Agent `bl` command), Setup & auth, and CLI errors: report an issue. Command details are authoritative in [`reference/`](reference/index.md) (dataset / finetune / deploy) and `bl --help` — do not guess flags. The whole pipeline requires an API key. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** ## End-to-end workflow (follow in order) diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 102912664..94ccb8aef 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -20,7 +20,7 @@ description: >- # Bailian media generation & understanding (`bl image` / `bl video` / `bl speech` / `bl omni` / `bl vision`) -**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent (one-time ask templates), Version & updates (pre-flight checklist), and CLI errors: report an issue. Command details are authoritative in [`reference/`](reference/index.md) and `bl --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** +**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent (one-time ask templates), Version & updates (pre-flight checklist), Call attribution (every Agent `bl` command), and CLI errors: report an issue. Command details are authoritative in [`reference/`](reference/index.md) and `bl --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** ## Consent (short version; full rules in bailian-protocol) diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 84331217e..b4942d8e7 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -18,7 +18,7 @@ description: >- # Bailian managed agent IaC (`bl managed-agent`) -**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): High-risk operation confirmation, Version & updates (pre-flight checklist), and CLI errors: report an issue. Command details are authoritative in [`reference/managed-agent.md`](reference/managed-agent.md) and `bl managed-agent --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** +**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): High-risk operation confirmation, Version & updates (pre-flight checklist), Call attribution (every Agent `bl` command), and CLI errors: report an issue. Command details are authoritative in [`reference/managed-agent.md`](reference/managed-agent.md) and `bl managed-agent --help` — do not guess flags. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** ## Safety guardrail (the most important rule) diff --git a/skills/bailian-memory/SKILL.md b/skills/bailian-memory/SKILL.md index 5cb67e466..6fbb3282b 100644 --- a/skills/bailian-memory/SKILL.md +++ b/skills/bailian-memory/SKILL.md @@ -19,7 +19,7 @@ description: >- **读取本 Skill、检查本地启用状态与发送云端请求是三个不同步骤。** 前两步不以已经启用为前提;只有云端个人记忆读写需要启用授权。安装 Skill 不代表用户已授权上传。 -执行任何 `bl` 命令前,先读取共享协议 [bailian-protocol](../bailian-protocol/SKILL.md),遵循鉴权、版本预检和高风险确认。协议缺失不妨碍识别记忆意图或解释启用流程;但执行命令前须补齐,提示 `bl skill init`,不能静默退回内置记忆并声称本服务已工作。 +执行任何 `bl` 命令前,先读取共享协议 [bailian-protocol](../bailian-protocol/SKILL.md),遵循鉴权、版本预检、调用归因和高风险确认。协议缺失不妨碍识别记忆意图或解释启用流程;但执行命令前须补齐,提示 `bl skill init`,不能静默退回内置记忆并声称本服务已工作。 ## 入口与范围 diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index dfacbb90d..663f10633 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -21,7 +21,7 @@ description: >- > bl skill init > ``` > -> 子集安装请显式带上 `bailian-protocol`(例如 `bl skill add --name bailian-protocol,bailian-gen`),否则业务 skill 的相对路径 Read 会失败。 +> 子集安装请显式带上 `bailian-protocol`(例如 `bl skill add --name bailian-protocol,bailian-gen`),否则业务 skill 的相对路径 Read 会失败。Agents wrap with [Call attribution](#call-attribution-mandatory-for-every-agent-bl-command) (`bailian-protocol`). ## Provider selection and consent @@ -72,9 +72,38 @@ After approval, treat Bailian as selected for the current task. Do not ask again - `bailian-protocol`:靠 `bl skill init` 与业务 skill 同装;CRITICAL 可用相对路径 `../bailian-protocol/…`。读不到则停止跑 `bl`,提示整包安装。 - 其它 bailian-\* 业务 skill:只按名字提及,**不要**写死 `../bailian-*/SKILL.md` 当执行前提。 +## Call attribution (mandatory for every Agent `bl` command) + +Examples in this protocol, `assets/`, and business skills show bare `bl …`. When an Agent **runs** a command, wrap that single invocation with the prefix below (declared once here). Humans must not set these variables; unset defaults to human. + +```bash +BAILIAN_CALLER_TYPE=agent \ +BAILIAN_CALLER_NAME= \ +BAILIAN_SKILL_NAME= \ +BAILIAN_SKILL_VERSION= \ +bl +``` + +Pick `` from the task (do not repeat the prefix in every example): + +| Task | `` | +| ------------------------------------------------------------------------ | -------------------------------------- | +| Install, `skill init` / `skill update`, `bl update`, protocol pre-flight | `bailian-protocol` | +| auth, config, text, app, knowledge, usage | `bailian-cli` | +| image / video / speech / vision / omni | `bailian-gen` | +| Other domain commands | The `bailian-*` skill currently in use | + +Conventions: + +- `BAILIAN_CALLER_NAME`: stable host id (`cursor` / `claude-code` / `codex` / `qwen-code`); use `unknown` when unsure. Do not scan install directories. +- `BAILIAN_SKILL_VERSION`: copy `metadata.version` from that owning skill's `SKILL.md`. +- Prefix each `bl` invocation; do not `export` into the shell session. `bl --version` / `--help` skip telemetry and need no prefix. +- If the host accepts one-shot env without rewriting the command string, set the same four variables there. +- PowerShell: fresh subprocess, or save/restore all four (restore to unset when they were unset). + ## Version & updates (after provider selection, before the first `bl` command) -**MANDATORY:** Before running any `bl` command, complete the **Agent pre-flight checklist** in [`assets/versioning.md`](assets/versioning.md). Do NOT run any `bl` command until the checklist is complete. If versions mismatch, ask the user whether to upgrade — do not proceed silently. +**MANDATORY:** Before running any `bl` command, complete the **Agent pre-flight checklist** in [`assets/versioning.md`](assets/versioning.md). Do NOT run any `bl` command until the checklist is complete. If versions mismatch, ask the user whether to upgrade — do not proceed silently. Pre-flight `bl` commands that enter middleware (for example `bl skill update` / `bl update` / `bl skill init`) must use the same call-attribution prefix above; owning skill for those is `bailian-protocol`. ## Setup & auth @@ -94,7 +123,7 @@ bl text chat --message "Write a poem about spring" # explicit text-model smoke ## Color output When an agent needs plain text without ANSI color codes (for parsing, logs, or -snapshots), run the command with `NO_COLOR=1`: +snapshots), run with `NO_COLOR=1` (and Call attribution when run by an Agent): ```bash NO_COLOR=1 bl config show --output text diff --git a/skills/bailian-protocol/assets/setup.md b/skills/bailian-protocol/assets/setup.md index d61e2c518..7c7cd840a 100644 --- a/skills/bailian-protocol/assets/setup.md +++ b/skills/bailian-protocol/assets/setup.md @@ -7,6 +7,8 @@ Read this only when you need to install `bl`, change credentials/endpoint, or inspect config keys. Day-to-day command routing lives in the business skills (`bailian-cli` / `bailian-gen` / …). +Examples stay bare `bl …`. Agents wrap them with [Call attribution](../SKILL.md#call-attribution-mandatory-for-every-agent-bl-command): `bailian-protocol` for install / `skill init`, `bailian-cli` for auth / config. + --- ## Install diff --git a/skills/bailian-protocol/assets/versioning.md b/skills/bailian-protocol/assets/versioning.md index 6de95f88c..b598eada0 100644 --- a/skills/bailian-protocol/assets/versioning.md +++ b/skills/bailian-protocol/assets/versioning.md @@ -3,6 +3,8 @@ > Hand-maintained. Lives in `bailian-protocol/assets/` (not auto-generated from command metadata). > Entry point: [SKILL.md → Version & updates](../SKILL.md#version--updates-after-provider-selection-before-the-first-bl-command). +Agents wrap middleware `bl` commands with [Call attribution](../SKILL.md#call-attribution-mandatory-for-every-agent-bl-command); `` here is always `bailian-protocol`. Examples stay bare. `bl --version` / `--help` need no prefix. + ## Agent pre-flight checklist (MANDATORY) **Do NOT run any `bl` command until you complete this checklist.** Run it **once per session**, before the first `bl` command. Cache the result — do not re-check before every command. @@ -18,7 +20,7 @@ ```bash bl skill update ``` - - If `bailian-protocol` or another needed skill is missing, stop and run `bl skill init`. Do not use `bl skill init` only to refresh versions — it installs every `bailian-*` skill. + - If `bailian-protocol` or another needed skill is missing, stop and run `bl skill init` (same Call attribution, `bailian-protocol`). Do not use `bl skill init` only to refresh versions — it installs every `bailian-*` skill. - Do not trust a stale `reference/` when versions mismatch — flags may be wrong. 4. Check the latest published CLI version: ```bash @@ -27,7 +29,10 @@ 5. If the installed `bl` is **older** than the latest npm version, **STOP** the current task and **ask the user** (report skill version, installed CLI version, and npm latest): > A newer version of bl is available (current: X.Y.Z, latest: A.B.C). Upgrade before continuing? - **Do NOT auto-upgrade silently** — the user decides. - - If the user agrees: run `bl update`, then continue. (`bl update` uses the detected install channel and, on success, also runs `bl skill update` to refresh already-installed skills; it does not install new skills. First-time install still uses `bl skill init`.) + - If the user agrees: run `bl update` (same Call attribution), then continue. (`bl update` uses the detected install channel and, on success, also runs `bl skill update` to refresh already-installed skills; it does not install new skills. First-time install still uses `bl skill init`.) + ```bash + bl update + ``` - If the user declines: continue with the current version and note it in the summary. - If `npm view` / `bl update` fails (offline, registry blocked, permission): continue with the current `bl` and tell the user it could not be updated. 6. Only proceed with the user's actual task after the above is resolved. diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index 86cdd80c1..7cf6bfca0 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -15,7 +15,7 @@ description: >- # Bailian Sandbox (`bl sandbox`) -Before running `bl`, read the shared [bailian-protocol](../bailian-protocol/SKILL.md) for consent, high-risk confirmation, version checks, authentication, and error handling. If it is missing, stop execution and prompt the user to install the full family with `bl skill init`. +Before running `bl`, read the shared [bailian-protocol](../bailian-protocol/SKILL.md) for consent, high-risk confirmation, version checks, call attribution, authentication, and error handling. If it is missing, stop execution and prompt the user to install the full family with `bl skill init`. ## Scope and setup diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 7dc93855c..1622d94a1 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -19,7 +19,7 @@ description: >- # Bailian web search routing (`bailian-web-search`) -**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent, Version & updates (pre-flight checklist), Setup & auth, and CLI errors: report an issue. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** +**CRITICAL — Before executing, MUST read the shared protocol in [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md): Provider selection and consent, Version & updates (pre-flight checklist), Call attribution (every Agent `bl` command), Setup & auth, and CLI errors: report an issue. If that protocol file is missing, stop and run `bl skill init`; do not guess auth/consent.** > **Scope:** model-aware routing for web search inside Agent-driven `bl` workflows — this skill owns the routing decision only. > Command flags / usage / examples: Read skill `bailian-cli` reference if installed; else `bl search web --help` / `bl text chat --help` — do not guess flags. From 2853389570d3294b06f535b6e0502ac66d79e496 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Thu, 22 Oct 2026 10:24:52 +0800 Subject: [PATCH 2/2] fix(telemetry): record unrecognized callers as unknown instead of human --- docs/agents/telemetry-change.md | 12 ++++++------ packages/core/src/telemetry/caller.ts | 6 +++--- packages/core/src/telemetry/event.ts | 2 +- packages/core/tests/telemetry-caller.test.ts | 8 ++++---- packages/core/tests/telemetry-tracker.test.ts | 10 +++++----- packages/runtime/tests/telemetry-stage.test.ts | 4 ++-- skills/bailian-protocol/SKILL.md | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/agents/telemetry-change.md b/docs/agents/telemetry-change.md index d8e6ed9f0..dd35b278d 100644 --- a/docs/agents/telemetry-change.md +++ b/docs/agents/telemetry-change.md @@ -95,17 +95,17 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网 - `command`、`timestamp`、`durationMs`、`success` - `cliVersion`、`nodeVersion`、`os` - `authMethod` -- `callerType`(必填:`agent` 或 `human`);`callerType=agent` 时可选 `callerName`、`skillName`、`skillVersion` +- `callerType`(必填:`agent` 或 `unknown`);`callerType=agent` 时可选 `callerName`、`skillName`、`skillVersion` - 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId` - 安全 allowlist 过滤后的 `params` -调用方归因只认本次进程的 `BAILIAN_CALLER_*`(由 `readCaller` 归一化后写入事件),口径对齐 Ark CLI: +调用方归因只认本次进程的 `BAILIAN_CALLER_*`(由 `readCaller` 归一化后写入事件): - `BAILIAN_CALLER_TYPE` 为 `agent` / `ai_agent`(大小写不敏感)→ `callerType=agent` -- 未设置、空白、显式 `human` 或其他值 → `callerType=human`;人类终端不要手动设这些变量 +- 未设置、空白或其他值 → `callerType=unknown`;不要把该字段当成人工 - `callerName` / `skillName` / `skillVersion` 仅在 `agent` 时发送;`kscli` 通常没有 skill 字段 -新 CLI 每条进入 middleware 的命令事件都有 `callerType`。旧 CLI 没有该字段。看板只统计带 `callerType` 的事件,不能把字段缺失混进分母。新 CLI + 旧 skill / Agent 漏设变量时,会落进 `human`(与 Ark 相同,靠 skill 强制贴标签收敛)。`BAILIAN_CALLER_*` 是调用方声明,不是 `Identity`,不得用来猜 `bl` / `kscli`。 +新 CLI 每条进入 middleware 的命令事件都有 `callerType`。旧 CLI 没有该字段。看板只统计带 `callerType` 的事件,不能把字段缺失混进分母。`unknown` 只表示未识别为 agent,不是人工。新 CLI + 旧 skill / Agent 漏设变量时,会落进 `unknown`。`BAILIAN_CALLER_*` 是调用方声明,不是 `Identity`,不得用来猜 `bl` / `kscli`。 参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。 @@ -158,7 +158,7 @@ AEM 映射: - [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射 - [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径 -- [ ] 调用方字段经 `readCaller` → `TrackingDeps.caller` → 事件;未设置默认 `human`,仅显式 Agent 声明为 `agent` +- [ ] 调用方字段经 `readCaller` → `TrackingDeps.caller` → 事件;仅显式 `agent` / `ai_agent` 为 `agent`,其余为 `unknown` - [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码 - [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5` - [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口 @@ -177,7 +177,7 @@ vp test packages/core/tests packages/runtime/tests/telemetry-stage.test.ts packa - ✗ 只看 AEM 命令事件,误以为它能替代网关侧请求渠道统计 - ✗ 把 `authMethod` 当成实际凭证来源;它只是命令声明的鉴权域 -- ✗ 把旧 CLI「没有 `callerType`」和「`callerType=human`」混进同一分母宣称人工占比 +- ✗ 把旧 CLI「没有 `callerType`」或「`callerType=unknown`」当成人工占比 - ✗ 用 `BAILIAN_CALLER_*` 猜测产品身份,或根据 TTY / 安装目录反推调用方 - ✗ 新增 bypass `fetch` 后漏掉应由网关消费的 source-config,或把它发给 OSS / npm / 第三方下载地址 - ✗ 只改 `bl` 入口,导致 `kscli` 的产品名或版本标签错误 diff --git a/packages/core/src/telemetry/caller.ts b/packages/core/src/telemetry/caller.ts index 6c1b1df30..8fbde3f66 100644 --- a/packages/core/src/telemetry/caller.ts +++ b/packages/core/src/telemetry/caller.ts @@ -1,7 +1,7 @@ -/** Parse BAILIAN_CALLER_* from the process env. Defaults to human. */ +/** Parse BAILIAN_CALLER_*. Only an explicit agent is `agent`; everything else is `unknown`. */ export type CallerAttribution = - | { callerType: "human" } + | { callerType: "unknown" } | { callerType: "agent"; callerName: string; @@ -36,7 +36,7 @@ function normalizeCallerName(raw: string | undefined): string { export function readCaller(env: NodeJS.ProcessEnv): CallerAttribution { const callerType = readTrimmed(env, CALLER_TYPE_ENV)?.toLowerCase(); if (callerType !== "agent" && callerType !== "ai_agent") { - return { callerType: "human" }; + return { callerType: "unknown" }; } const skillNameRaw = readTrimmed(env, SKILL_NAME_ENV)?.toLowerCase(); diff --git a/packages/core/src/telemetry/event.ts b/packages/core/src/telemetry/event.ts index 911ba2b63..bc31e60dd 100644 --- a/packages/core/src/telemetry/event.ts +++ b/packages/core/src/telemetry/event.ts @@ -15,7 +15,7 @@ export interface TrackingEvent { os: string; authMethod?: AuthRequirement; params?: Record; - /** Always present on new CLI events: `agent` or `human`. */ + /** Always present on new CLI events: `agent` or `unknown`. */ callerType: CallerAttribution["callerType"]; callerName?: string; skillName?: string; diff --git a/packages/core/tests/telemetry-caller.test.ts b/packages/core/tests/telemetry-caller.test.ts index 057a578af..e949e6305 100644 --- a/packages/core/tests/telemetry-caller.test.ts +++ b/packages/core/tests/telemetry-caller.test.ts @@ -8,8 +8,8 @@ test.each([ { label: "illegal false", env: { BAILIAN_CALLER_TYPE: "false" } }, { label: "illegal ai-agent", env: { BAILIAN_CALLER_TYPE: "ai-agent" } }, { label: "illegal zero", env: { BAILIAN_CALLER_TYPE: "0" } }, -])("maps $label to human without agent fields", ({ env }) => { - expect(readCaller(env)).toEqual({ callerType: "human" }); +])("maps $label to unknown without agent fields", ({ env }) => { + expect(readCaller(env)).toEqual({ callerType: "unknown" }); }); test.each([ @@ -130,7 +130,7 @@ test("lowercases skillName before validation", () => { }); }); -test("ignores agent detail fields when callerType is human", () => { +test("ignores agent detail fields when callerType is not agent", () => { expect( readCaller({ BAILIAN_CALLER_TYPE: "human", @@ -138,5 +138,5 @@ test("ignores agent detail fields when callerType is human", () => { BAILIAN_SKILL_NAME: "bailian-cli", BAILIAN_SKILL_VERSION: "2.5.0", }), - ).toEqual({ callerType: "human" }); + ).toEqual({ callerType: "unknown" }); }); diff --git a/packages/core/tests/telemetry-tracker.test.ts b/packages/core/tests/telemetry-tracker.test.ts index c623806a6..f36da576d 100644 --- a/packages/core/tests/telemetry-tracker.test.ts +++ b/packages/core/tests/telemetry-tracker.test.ts @@ -21,7 +21,7 @@ const identity: Identity = { npmPackage: "bailian-cli", }; -const humanCaller: CallerAttribution = { callerType: "human" }; +const unknownCaller: CallerAttribution = { callerType: "unknown" }; const agentCaller: CallerAttribution = { callerType: "agent", callerName: "cursor", @@ -40,7 +40,7 @@ test("records BailianError exitCode in local events and AEM ext", async () => { identity, settings: { telemetry: true } as Settings, authMethod: "apiKey", - caller: humanCaller, + caller: unknownCaller, }, ["knowledge", "doc", "delete"], {}, @@ -57,13 +57,13 @@ test("records BailianError exitCode in local events and AEM ext", async () => { success: false, exitCode: 7, errorMessage: "该操作会永久删除文档。", - callerType: "human", + callerType: "unknown", }); expect(event.callerName).toBeUndefined(); expect(event.skillName).toBeUndefined(); expect(event.skillVersion).toBeUndefined(); expect(buildRemoteAemOptions(event)).toMatchObject({ - ext: expect.objectContaining({ exitCode: 7, callerType: "human" }), + ext: expect.objectContaining({ exitCode: 7, callerType: "unknown" }), }); }); @@ -85,7 +85,7 @@ test.each([ identity, settings: { telemetry: true } as Settings, authMethod: "apiKey", - caller: humanCaller, + caller: unknownCaller, }, ["managed-agent", "webhook", "events"], { page, pageSize: 20 }, diff --git a/packages/runtime/tests/telemetry-stage.test.ts b/packages/runtime/tests/telemetry-stage.test.ts index 1e3133298..c575441eb 100644 --- a/packages/runtime/tests/telemetry-stage.test.ts +++ b/packages/runtime/tests/telemetry-stage.test.ts @@ -71,7 +71,7 @@ test("wires process.env through readCaller into trackCommandExecution", async () expect(next).toHaveBeenCalledOnce(); }); -test("defaults to human when caller env is unset", async () => { +test("defaults to unknown when caller env is unset", async () => { vi.stubEnv("BAILIAN_CALLER_TYPE", undefined); vi.stubEnv("BAILIAN_CALLER_NAME", undefined); vi.stubEnv("BAILIAN_SKILL_NAME", undefined); @@ -83,5 +83,5 @@ test("defaults to human when caller env is unset", async () => { const deps = trackerMocks.trackCommandExecution.mock.calls[0]![0] as { caller: CallerAttribution; }; - expect(deps.caller).toEqual({ callerType: "human" }); + expect(deps.caller).toEqual({ callerType: "unknown" }); }); diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 663f10633..74b408887 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -74,7 +74,7 @@ After approval, treat Bailian as selected for the current task. Do not ask again ## Call attribution (mandatory for every Agent `bl` command) -Examples in this protocol, `assets/`, and business skills show bare `bl …`. When an Agent **runs** a command, wrap that single invocation with the prefix below (declared once here). Humans must not set these variables; unset defaults to human. +Examples in this protocol, `assets/`, and business skills show bare `bl …`. When an Agent **runs** a command, wrap that single invocation with the prefix below (declared once here). Humans must not set these variables. Anything other than an explicit agent is recorded as `unknown`. ```bash BAILIAN_CALLER_TYPE=agent \