coze_lab 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,9 @@ created after a new Codex turn, Codex did not load or execute the hook.
69
69
  ## Token lifecycle
70
70
 
71
71
  OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
72
+ For local `--agent-id=<agentId>` setup, if
73
+ `~/.coze/agents/<agentId>/config.json` contains `patToken`, onboarding uses that
74
+ PAT directly and skips the OAuth device-code flow.
72
75
 
73
76
  In cloud mode, trace verification and hook uploads prefer
74
77
  `COZELOOP_API_TOKEN` and fall back to `COZE_API_TOKEN`. The selfcheck result is
@@ -77,7 +80,8 @@ writes the hook configuration but reports `verify=fail` with `token_source`.
77
80
  For Python SDK uploads, `OTEL_ENDPOINT` is not used as the SDK base URL; set
78
81
  `COZELOOP_API_BASE_URL` only when the SDK ingest endpoint should be overridden.
79
82
 
80
- **At hook execution time** (Claude Code / Codex), the Python hook script automatically:
83
+ **At hook execution time** (Claude Code / Codex), per-agent PAT setups use the
84
+ token written by onboarding. Otherwise, the Python hook script automatically:
81
85
  1. Reads `~/.cozeloop/credentials.json`
82
86
  2. If the token expires in < 10 minutes, calls the Coze refresh API
83
87
  3. Updates `credentials.json` with the new token
package/index.js CHANGED
@@ -9,6 +9,7 @@ const CREDS_PATH = require('path').join(require('os').homedir(), '.cozeloop', '
9
9
  const PACKAGE_VERSION = require('./package.json').version;
10
10
  // Refresh when less than 10 minutes remain
11
11
  const REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
12
+ const TOKEN_SOURCE_AGENT_PAT = 'agent_config.patToken';
12
13
 
13
14
  // ─── 1. Cloud structured output ──────────────────────────────────────────────
14
15
  // 云端模式:在 stdout 输出一行机器可读结果 COZE_LAB_RESULT={...},
@@ -176,6 +177,10 @@ function hasCloudModelInfo(cfg) {
176
177
  return !!(cfg?.modelInfo && typeof cfg.modelInfo === 'object' && !Array.isArray(cfg.modelInfo));
177
178
  }
178
179
 
180
+ function getAgentPatToken(cfg) {
181
+ return typeof cfg?.patToken === 'string' ? cfg.patToken.trim() : '';
182
+ }
183
+
179
184
  function inferDeployTypeFromAgentConfig(cfg) {
180
185
  if (cfg?.deployType === 'cloud') return { deployType: 'cloud', reason: 'config.deployType=cloud' };
181
186
  if (isCloudRuntimeEnv()) return { deployType: 'cloud', reason: 'env CLOUD_ENV=1' };
@@ -216,7 +221,7 @@ function resolveAgent(agentId, soft) {
216
221
  ]);
217
222
  }
218
223
  const inferred = inferDeployTypeFromAgentConfig(cfg);
219
- return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root };
224
+ return { framework, workspace: cfg.workspace || '', deployType: inferred.deployType, deployReason: inferred.reason, agentId, root, patToken: getAgentPatToken(cfg) };
220
225
  }
221
226
 
222
227
  function validateArgs(args) {
@@ -239,6 +244,7 @@ function validateArgs(args) {
239
244
  agentId: resolved.agentId,
240
245
  workspace: resolved.workspace,
241
246
  agentRoot: resolved.root,
247
+ patToken: resolved.patToken,
242
248
  deployType: resolved.deployType,
243
249
  deployReason: explicitCloud ? '--cloud' : resolved.deployReason,
244
250
  'codex-home': args['codex-home'],
@@ -527,7 +533,7 @@ function shellEnvLine(key, value) {
527
533
  // writeClaudeCodeHook 配置 Claude Code 的 hook。
528
534
  // configBaseDir 缺省 process.cwd()(全局/项目级);传入 agent 的 workspace 则 per-agent:
529
535
  // settings.json + 凭证写进 <configBaseDir>/.claude,仅该 agent(以此为 cwd 启动)生效。
530
- function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cloud) {
536
+ function writeClaudeCodeHook(token, workspaceId, pythonCmd, configBaseDir, cloud, tokenSource) {
531
537
  const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
532
538
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
533
539
  const baseDir = configBaseDir || process.cwd();
@@ -610,9 +616,16 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
610
616
  existing.env.COZE_API_TOKEN = cozeToken;
611
617
  delete existing.env.COZELOOP_API_TOKEN;
612
618
  }
619
+ delete existing.env.COZELOOP_TOKEN_SOURCE;
620
+ } else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
621
+ existing.env.COZELOOP_API_TOKEN = token;
622
+ existing.env.COZELOOP_TOKEN_SOURCE = TOKEN_SOURCE_AGENT_PAT;
623
+ delete existing.env.COZE_API_TOKEN;
624
+ delete existing.env.COZELAB_ONBOARD_CLOUD;
613
625
  } else {
614
626
  delete existing.env.COZELOOP_API_TOKEN;
615
627
  delete existing.env.COZE_API_TOKEN;
628
+ delete existing.env.COZELOOP_TOKEN_SOURCE;
616
629
  delete existing.env.COZELAB_ONBOARD_CLOUD;
617
630
  }
618
631
  const loopBaseUrl = readEnv('COZELOOP_API_BASE_URL');
@@ -645,9 +658,10 @@ function writeClaudeCodeHook(patToken, workspaceId, pythonCmd, configBaseDir, cl
645
658
 
646
659
  // writeCodexHook 把 hook 写进指定的 CODEX_HOME。codexHome 缺省 ~/.codex;
647
660
  // 传入动态目录(如 coze-bridge 的 /tmp/coze-bridge-codex-home-xxx)即可 per-agent 生效。
648
- // 本地模式不把短期 token 写入 cozeloop.env;Hook 运行时读取 ~/.cozeloop/credentials.json。
661
+ // 普通本地模式不把短期 token 写入 cozeloop.env;Hook 运行时读取 ~/.cozeloop/credentials.json。
662
+ // 本地 --agent-id 且 config.patToken 存在时写入该 PAT,并用 COZELOOP_TOKEN_SOURCE 标记来源。
649
663
  // cloud=true 时写 COZELAB_ONBOARD_CLOUD,并带入 sandbox 注入的 trace token。
650
- function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
664
+ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud, tokenSource) {
651
665
  const home = codexHome || path.join(os.homedir(), '.codex');
652
666
  const hooksDir = path.join(home, 'hooks');
653
667
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
@@ -675,6 +689,9 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
675
689
  } else if (cozeToken) {
676
690
  envLines.push(shellEnvLine('COZE_API_TOKEN', cozeToken));
677
691
  }
692
+ } else if (tokenSource === TOKEN_SOURCE_AGENT_PAT && token) {
693
+ envLines.push(shellEnvLine('COZELOOP_API_TOKEN', token));
694
+ envLines.push(shellEnvLine('COZELOOP_TOKEN_SOURCE', TOKEN_SOURCE_AGENT_PAT));
678
695
  }
679
696
  envLines.push(shellEnvLine('CODEX_HOME', home));
680
697
  envLines.push(shellEnvLine('COZELOOP_HOOK_LOG', logFile));
@@ -879,7 +896,7 @@ function buildOpenClawSelfcheckTags(pcfg, workspaceId, pair) {
879
896
  return tags;
880
897
  }
881
898
 
882
- function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile) {
899
+ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource) {
883
900
  if (!existing.plugins) existing.plugins = {};
884
901
  if (!existing.plugins.allow) existing.plugins.allow = [];
885
902
  if (!existing.plugins.entries) existing.plugins.entries = {};
@@ -906,7 +923,12 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
906
923
  if (logFile) {
907
924
  pcfg.logFile = logFile;
908
925
  }
909
- pcfg.disableLocalCredentials = !!cloud;
926
+ if (tokenSource === TOKEN_SOURCE_AGENT_PAT) {
927
+ pcfg.tokenSource = TOKEN_SOURCE_AGENT_PAT;
928
+ } else {
929
+ delete pcfg.tokenSource;
930
+ }
931
+ pcfg.disableLocalCredentials = !!cloud || tokenSource === TOKEN_SOURCE_AGENT_PAT;
910
932
  // 插件代码版本:参与幂等比对,升级插件(bump scripts/openclaw/package.json version)后
911
933
  // 强制触发重写+重装+重启,避免云端 pluginDir 滞留旧插件 dist。
912
934
  if (OPENCLAW_PLUGIN_VERSION) {
@@ -924,7 +946,7 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud,
924
946
  return existing;
925
947
  }
926
948
 
927
- function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile) {
949
+ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource) {
928
950
  if (!fs.existsSync(pluginDir)) return false;
929
951
  let existing;
930
952
  try {
@@ -939,11 +961,12 @@ function isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, ag
939
961
  agentId,
940
962
  cloud,
941
963
  logFile,
964
+ tokenSource,
942
965
  );
943
966
  return JSON.stringify(existing) === JSON.stringify(desired);
944
967
  }
945
968
 
946
- function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
969
+ function writeOpenClawHook(token, workspaceId, agentId, cloud, force, tokenSource) {
947
970
  const home = resolveHomeDir(cloud);
948
971
  const configPath = path.join(home, '.openclaw', 'openclaw.json');
949
972
  const pluginDir = path.join(home, '.cozeloop', 'openclaw-plugin');
@@ -958,7 +981,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
958
981
  ]);
959
982
  }
960
983
 
961
- if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile)) {
984
+ if (!force && isOpenClawAlreadyInjected(configPath, pluginDir, token, workspaceId, agentId, cloud, logFile, tokenSource)) {
962
985
  ok(`OpenClaw plugin already configured in ${configPath}`);
963
986
  info('OpenClaw gateway restart skipped (configuration unchanged).');
964
987
  return { configPath, pluginDir, logFile, unchanged: true, gatewayRestarted: false, gatewayRestartSkipped: true };
@@ -1017,7 +1040,7 @@ function writeOpenClawHook(token, workspaceId, agentId, cloud, force) {
1017
1040
 
1018
1041
  // 4. Update openclaw.json with token and workspace
1019
1042
  const config = mergeJson(configPath, (existing) => {
1020
- return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile);
1043
+ return applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud, logFile, tokenSource);
1021
1044
  });
1022
1045
 
1023
1046
  try {
@@ -1886,7 +1909,8 @@ async function main() {
1886
1909
  // Step 1: Authorize.
1887
1910
  // 云端模式:token 取自 sandbox 注入的环境变量,跳过 OAuth / credentials.json。
1888
1911
  // 优先使用 COZELOOP_API_TOKEN;兼容使用 COZE_API_TOKEN,并以真实 selfcheck 为准。
1889
- // 本地:load cached refresh device code
1912
+ // 本地 --agent-id config.patToken 存在:优先使用该 PAT,跳过 OAuth / credentials.json
1913
+ // 其它本地模式:load cached → refresh → device code。
1890
1914
  // 注意:workspace_id 始终用写死的 WORKSPACE_ID(团队固定上报 workspace),不读环境。
1891
1915
  let token;
1892
1916
  let tokenSource = '';
@@ -1910,8 +1934,14 @@ async function main() {
1910
1934
  }
1911
1935
  } else {
1912
1936
  info('Step 1/5: 检查授权状态...');
1913
- token = await getValidToken();
1914
- tokenSource = 'credentials';
1937
+ if (args.agentId && args.patToken) {
1938
+ token = args.patToken;
1939
+ tokenSource = TOKEN_SOURCE_AGENT_PAT;
1940
+ ok(`已从 ~/.coze/agents/${args.agentId}/config.json 读取 patToken,跳过本地授权。`);
1941
+ } else {
1942
+ token = await getValidToken();
1943
+ tokenSource = 'credentials';
1944
+ }
1915
1945
  }
1916
1946
  console.log('');
1917
1947
 
@@ -1945,7 +1975,7 @@ async function main() {
1945
1975
  '否则 hook 配置会落到全局/进程目录,无法做到 per-agent 隔离。',
1946
1976
  ]);
1947
1977
  }
1948
- written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud);
1978
+ written = writeClaudeCodeHook(token, WORKSPACE_ID, pythonCmd, args.agentId ? agentWorkspace : undefined, args.cloud, tokenSource);
1949
1979
  } else if (agent === 'codex') {
1950
1980
  const codexHome = resolveCodexHome(args);
1951
1981
  if (args.cloud && args.agentId && codexHome && !fs.existsSync(codexHome)) {
@@ -1953,10 +1983,10 @@ async function main() {
1953
1983
  info(`已创建云端 Codex 配置目录: ${codexHome}`);
1954
1984
  }
1955
1985
  if (codexHome) info(`Codex 配置目录: ${codexHome}`);
1956
- written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud);
1986
+ written = writeCodexHook(token, WORKSPACE_ID, pythonCmd, codexHome, args.cloud, tokenSource);
1957
1987
  } else {
1958
1988
  // openclaw:云端用 traceAgentIds allowlist 做 per-agent 放行。
1959
- written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force) || {};
1989
+ written = writeOpenClawHook(token, WORKSPACE_ID, args.agentId, args.cloud, args.force, tokenSource) || {};
1960
1990
  }
1961
1991
  // 走到这里说明 detectAgent / 环境检查 / 写 hook 配置全部成功 → 注入成功。
1962
1992
  cloudResult.inject = 'ok';
@@ -2056,6 +2086,7 @@ if (require.main === module) {
2056
2086
 
2057
2087
  module.exports = {
2058
2088
  resolveHomeDir,
2089
+ getAgentPatToken,
2059
2090
  mergeJson,
2060
2091
  atomicWriteFileSync,
2061
2092
  isExpired,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -369,6 +369,9 @@ def force_refresh_token_after_upload_failure(reason: str = "upload_failure", cur
369
369
  if is_cloud:
370
370
  hook_log("upload failure token refresh skipped in cloud mode")
371
371
  return None
372
+ if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken":
373
+ hook_log("upload failure token refresh skipped for agent config patToken")
374
+ return None
372
375
 
373
376
  creds = _load_credentials()
374
377
  if not creds or not creds.get("refresh_token"):
@@ -461,6 +464,8 @@ def get_fresh_token() -> Optional[str]:
461
464
  env_token = os.environ.get("COZELOOP_API_TOKEN")
462
465
  env_coze_token = os.environ.get("COZE_API_TOKEN")
463
466
  is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
467
+ if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken" and env_token:
468
+ return env_token
464
469
  if is_cloud:
465
470
  return env_token or env_coze_token or _token_from_credentials()
466
471
 
@@ -279,6 +279,9 @@ def force_refresh_token_after_upload_failure(reason: str = "upload_failure", cur
279
279
  if is_cloud:
280
280
  hook_log("upload failure token refresh skipped in cloud mode")
281
281
  return None
282
+ if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken":
283
+ hook_log("upload failure token refresh skipped for agent config patToken")
284
+ return None
282
285
 
283
286
  creds = _load_credentials()
284
287
  if not creds or not creds.get("refresh_token"):
@@ -370,6 +373,8 @@ def get_fresh_token():
370
373
  env_token = os.environ.get("COZELOOP_API_TOKEN")
371
374
  env_coze_token = os.environ.get("COZE_API_TOKEN")
372
375
  is_cloud = os.environ.get("COZELAB_ONBOARD_CLOUD", "").lower() in ("1", "true", "yes")
376
+ if os.environ.get("COZELOOP_TOKEN_SOURCE") == "agent_config.patToken" and env_token:
377
+ return env_token
373
378
  if is_cloud:
374
379
  return env_token or env_coze_token or _token_from_credentials()
375
380
 
@@ -187,6 +187,7 @@ async function _forceRefreshToken(currentAuthorization, opts = {}) {
187
187
  }
188
188
 
189
189
  async function getRefreshedToken(currentAuthorization, opts = {}) {
190
+ if (opts.tokenSource === "agent_config.patToken") return currentAuthorization;
190
191
  if (opts.disableLocalCredentials) return currentAuthorization;
191
192
  if (opts.force) return _forceRefreshToken(currentAuthorization, opts);
192
193
  const creds = _loadCreds();
@@ -565,6 +566,7 @@ export class CozeloopExporter {
565
566
  async refreshAuthIfNeeded() {
566
567
  const fresh = await getRefreshedToken(this.config.authorization, {
567
568
  disableLocalCredentials: this.config.disableLocalCredentials,
569
+ tokenSource: this.config.tokenSource,
568
570
  });
569
571
  if (fresh && fresh !== this.config.authorization) {
570
572
  this.api.logger.info("[CozeloopTrace] Token refreshed, re-initializing exporter...");
@@ -585,6 +587,7 @@ export class CozeloopExporter {
585
587
  async refreshAuthAfterUploadFailure(currentAuthorization, err) {
586
588
  const fresh = await getRefreshedToken(currentAuthorization || this.config.authorization, {
587
589
  disableLocalCredentials: this.config.disableLocalCredentials,
590
+ tokenSource: this.config.tokenSource,
588
591
  force: true,
589
592
  reason: err?.message || "upload_failure",
590
593
  logFile: this.config.logFile,
@@ -540,6 +540,7 @@ const cozeloopTracePlugin = {
540
540
  batchInterval: pluginConfig.batchInterval || 500,
541
541
  enabledHooks: pluginConfig.enabledHooks,
542
542
  disableLocalCredentials: pluginConfig.disableLocalCredentials === true,
543
+ tokenSource: pluginConfig.tokenSource,
543
544
  logFile: pluginConfig.logFile,
544
545
  };
545
546
  const exporter = new CozeloopExporter(api, config);