@itpay/cli 2.1.19 → 2.1.29

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.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/dist/src/commands/buy.js +3 -3
  3. package/dist/src/commands/cart.js +15 -15
  4. package/dist/src/commands/catalog.js +10 -7
  5. package/dist/src/commands/checkout.js +8 -11
  6. package/dist/src/commands/docs.js +3 -14
  7. package/dist/src/commands/guidance.js +41 -25
  8. package/dist/src/commands/install.js +2 -2
  9. package/dist/src/commands/order.js +4 -3
  10. package/dist/src/commands/orders.js +4 -9
  11. package/dist/src/commands/pay.js +1 -1
  12. package/dist/src/commands/rail_catalog.js +160 -11
  13. package/dist/src/commands/refund.js +5 -5
  14. package/dist/src/commands/services.js +658 -361
  15. package/dist/src/commands/skill.js +1 -1
  16. package/dist/src/commands/vault.js +27 -20
  17. package/dist/src/main.js +39 -24
  18. package/dist/src/render/markdown.js +0 -3
  19. package/dist/src/state/account_auth.js +9 -5
  20. package/dist/src/state/cart_session.js +4 -1
  21. package/dist/src/state/config.js +1 -1
  22. package/dist/src/state/task_journal.js +2 -0
  23. package/docs/agent/buyer/cart-checkout.json +5 -4
  24. package/docs/agent/buyer/catalog-list.json +24 -6
  25. package/docs/agent/buyer/identity-and-sessions.json +16 -15
  26. package/docs/agent/buyer/install-and-setup.json +5 -4
  27. package/docs/agent/buyer/orders-refunds.json +11 -5
  28. package/docs/agent/buyer/payment-flow.json +3 -3
  29. package/docs/agent/buyer/purchased-content.json +18 -4
  30. package/docs/agent/buyer/quickstart.json +22 -14
  31. package/docs/agent/buyer/rail-booking.json +125 -171
  32. package/docs/agent/buyer/rail-fast-checkout.json +21 -44
  33. package/docs/agent/buyer/render-hosts.json +9 -6
  34. package/docs/agent/buyer/service-feedback.json +2 -1
  35. package/docs/cli-reference/agent-types.md +1 -1
  36. package/docs/cli-reference/commands/auth.md +2 -0
  37. package/docs/cli-reference/commands/buy.md +1 -1
  38. package/docs/cli-reference/commands/cart/add.md +1 -1
  39. package/docs/cli-reference/commands/cart/clear.md +1 -1
  40. package/docs/cli-reference/commands/cart/index.md +1 -1
  41. package/docs/cli-reference/commands/cart/next.md +1 -1
  42. package/docs/cli-reference/commands/cart/remove.md +1 -1
  43. package/docs/cli-reference/commands/cart/show.md +1 -1
  44. package/docs/cli-reference/commands/catalog/index.md +1 -1
  45. package/docs/cli-reference/commands/catalog/list.md +1 -1
  46. package/docs/cli-reference/commands/checkout.md +1 -1
  47. package/docs/cli-reference/commands/device.md +8 -7
  48. package/docs/cli-reference/commands/docs/index.md +1 -1
  49. package/docs/cli-reference/commands/docs/list.md +1 -1
  50. package/docs/cli-reference/commands/docs/search.md +1 -1
  51. package/docs/cli-reference/commands/docs/show.md +1 -1
  52. package/docs/cli-reference/commands/install.md +2 -2
  53. package/docs/cli-reference/commands/next.md +1 -1
  54. package/docs/cli-reference/commands/order.md +1 -1
  55. package/docs/cli-reference/commands/pay.md +1 -1
  56. package/docs/cli-reference/commands/readyz.md +1 -1
  57. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  58. package/docs/cli-reference/commands/refund/create.md +1 -1
  59. package/docs/cli-reference/commands/refund/get.md +2 -2
  60. package/docs/cli-reference/commands/refund/index.md +1 -1
  61. package/docs/cli-reference/commands/refund/list.md +1 -1
  62. package/docs/cli-reference/commands/refund/watch.md +1 -1
  63. package/docs/cli-reference/commands/services/action.md +3 -3
  64. package/docs/cli-reference/commands/services/checkout.md +1 -1
  65. package/docs/cli-reference/commands/services/events.md +1 -1
  66. package/docs/cli-reference/commands/services/get.md +1 -1
  67. package/docs/cli-reference/commands/services/index.md +1 -1
  68. package/docs/cli-reference/commands/services/invoke.md +5 -13
  69. package/docs/cli-reference/commands/services/list.md +1 -1
  70. package/docs/cli-reference/commands/services/next.md +12 -10
  71. package/docs/cli-reference/commands/services/page.md +9 -7
  72. package/docs/cli-reference/commands/services/quote.md +1 -1
  73. package/docs/cli-reference/commands/services/read-result.md +16 -9
  74. package/docs/cli-reference/commands/services/run.md +4 -2
  75. package/docs/cli-reference/commands/services/start.md +1 -1
  76. package/docs/cli-reference/conventions.md +2 -2
  77. package/package.json +7 -5
  78. package/skills/itpay/SKILL.md +77 -238
  79. package/docs/reviews/agent-ux-handoff-2026-09-23/IMPLEMENTATION-EVIDENCE.md +0 -174
  80. package/docs/skill-bundle-rollout/01-mcp-authentication.md +0 -134
  81. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +0 -210
  82. package/docs/skill-bundle-rollout/03-platform-publishing.md +0 -161
  83. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +0 -70
  84. package/docs/skill-bundle-rollout/05-sync-operations.md +0 -62
  85. package/docs/skill-bundle-rollout/README.md +0 -151
  86. /package/bin/{itp → itp.mjs} +0 -0
@@ -24,7 +24,7 @@ export function runSkillShow(name, options = {}) {
24
24
  result: { skill: ITPAY_SKILL, content },
25
25
  instruction: agentType
26
26
  ? agentType === "workbuddy"
27
- ? "完整读取并遵守 Skill;把自己视为服务用户的代理,先理解用户要购买新服务、查看已购内容、查询订单还是处理退款,并先解释用户权益,再选择对应的第一条命令。保持 workbuddy 和同一 Node/CLI launcher。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。"
27
+ ? "完整读取并遵守 Skill;按用户意图选择第一条命令。保持 workbuddy 和同一 CLI launcher,并为 ~/.itpay-v3 申请宿主允许的持久化权限;权限被拒时报告受阻操作,不删除 identity。"
28
28
  : `完整读取并遵守 Skill;把自己视为服务用户的代理,先理解用户意图和解释用户权益,再选择购买、已购内容、订单或退款对应的第一条命令。当前 Agent Type 是 ${agentType},后续命令保持不变。`
29
29
  : "完整读取并遵守 Skill;先如实选择当前运行环境对应的 Agent Type。",
30
30
  next: agentType
@@ -1,7 +1,7 @@
1
1
  import { HttpError } from "../client/http.js";
2
- import { requiresTarget } from "../state/client_context.js";
2
+ import { requiresTarget, validateContext } from "../state/client_context.js";
3
3
  import { formatMoney } from "../render/output.js";
4
- import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
4
+ import { CommandContractError, shellArgument, writeCommandEnvelope } from "./guidance.js";
5
5
  import { buildVaultHandoff } from "./vault_handoff.js";
6
6
  function outputOptions(options, plainResult) {
7
7
  return {
@@ -19,21 +19,21 @@ export async function runVaultList(backend, input) {
19
19
  const value = await backend.listBuyerVaultArtifacts(input);
20
20
  writeCommandEnvelope({
21
21
  status: value.items.length ? "vault_listed" : "no_vault_artifacts",
22
- result: { items: value.items, next_cursor: value.next_cursor || null },
22
+ result: { items: value.items.map(item => ({ ...item, amount: formatMoney(item.amount_minor, item.currency), reader: { command: vaultReadCommand(item.artifact_ref, [], input), reason: "读取这一已购内容" } })), next_cursor: value.next_cursor || null },
23
23
  instruction: value.items.length
24
- ? "用编号、服务名称、内容主体、购买时间、金额和订单号说明匹配结果,不要向用户显示内部内容标识。一个精确匹配可按用户原始查看意图继续读取;多个匹配必须让用户选择。"
24
+ ? "用编号、服务名称、内容主体、购买时间、金额和订单号说明匹配结果,不要向用户显示内部内容标识。一个精确匹配可按用户原始查看意图继续读取;已有任务可明确匹配时读取对应行;只有真实歧义才让用户选择。"
25
25
  : "当前账号没有匹配的已购内容。向用户说明没有找到,不要猜测内容标识、自动购买或发起新的服务查询。",
26
- next: null,
26
+ next: value.next_cursor ? { command: `itpay vault list --limit ${input.limit}${input.query ? ` --query ${shellArgument(input.query)}` : ""} --cursor ${shellArgument(value.next_cursor)}${contextFlags(input)} --json`, reason: "读取同一筛选的下一页" } : null,
27
27
  recovery: [],
28
- }, outputOptions(input, value.items.map((item, index) => `${index + 1}. ${item.service_title}${item.subject_label ? ` · ${item.subject_label}` : ""} · ${formatMoney(item.amount_minor, item.currency)} · ${item.purchased_at} · ${item.order_code} · ${item.order_status}`)));
28
+ }, outputOptions(input));
29
29
  }
30
30
  catch (error) {
31
31
  if (error instanceof HttpError && error.code === "vault_authorization_required") {
32
32
  writeCommandEnvelope({
33
33
  status: "human_authorization_required",
34
34
  result: { intent: "list_purchased_content", query: input.query ?? "" },
35
- instruction: `需要用户确认一次身份和只读权限。执行 next.command 生成官方入口,不要声称链接已经创建;用户完成后重新运行原始 vault list 命令。${accessContextInstruction(input)}`,
36
- next: { command: vaultAccessCommand(undefined, input), reason: "创建一次账号读取授权" }, recovery: [],
35
+ instruction: `需要用户确认一次身份和只读权限。按当前动作或待填模板生成官方入口,不要声称链接已经创建;用户完成后重新运行原始 vault list 命令。${accessContextInstruction(input)}`,
36
+ ...vaultAccessGuidance(undefined, input), recovery: [],
37
37
  }, outputOptions(input));
38
38
  return;
39
39
  }
@@ -41,6 +41,9 @@ export async function runVaultList(backend, input) {
41
41
  }
42
42
  }
43
43
  export async function runVaultAccess(backend, artifactRef, options) {
44
+ const issue = validateContext(options.host, options.target);
45
+ if (issue)
46
+ throw new CommandContractError(issue.code, issue.message, "先补齐当前可信会话的真实宿主和目标;尚未创建授权请求。", [], { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command: vaultAccessCommand(artifactRef, options), required_input: issue.code === "target_required" ? ["target"] : ["host"], executable: false } });
44
47
  const value = await backend.createVaultAccessRequest(artifactRef
45
48
  ? { purpose: "artifact_reveal", artifact_ref: artifactRef }
46
49
  : { purpose: "account_window" });
@@ -90,23 +93,23 @@ export async function runVaultRead(backend, artifactRef, sections, options) {
90
93
  : value.status === "result_preparing"
91
94
  ? "这份已购内容仍在准备。稍后只重试同一 read,不要重新授权、购买或调用 Provider。"
92
95
  : "这份已购内容当前不可用。停止,不要重试、重新购买或绕过退款锁。",
93
- next: null, recovery: [],
96
+ next: value.status === "result_preparing" ? { command: vaultReadCommand(artifactRef, normalized, options), reason: "稍后读取同一内容及原sections;不重新授权或购买" } : null, recovery: [],
94
97
  }, outputOptions(options));
95
98
  }
96
99
  catch (error) {
97
100
  if (error instanceof HttpError && error.code === "artifact_authorization_required") {
98
101
  writeCommandEnvelope({
99
102
  status: "human_authorization_required", result: { artifact_ref: artifactRef },
100
- instruction: `这份内容需要用户单独确认读取权限。执行 next.command 生成一次官方入口;用户完成后重新运行原始 read,不要重复创建授权请求。${accessContextInstruction(options)}`,
101
- next: { command: vaultAccessCommand(artifactRef, options), reason: "创建一次内容读取授权" }, recovery: [],
103
+ instruction: `这份内容需要用户单独确认读取权限。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read,不要重复创建授权请求。${accessContextInstruction(options)}`,
104
+ ...vaultAccessGuidance(artifactRef, options), recovery: [],
102
105
  }, outputOptions(options));
103
106
  return;
104
107
  }
105
108
  if (error instanceof HttpError && error.code === "vault_authorization_required") {
106
109
  writeCommandEnvelope({
107
110
  status: "human_authorization_required", result: { artifact_ref: artifactRef },
108
- instruction: `账号读取授权已缺失或过期。执行 next.command 生成一次官方入口;用户完成后重新运行原始 read。${accessContextInstruction(options)}`,
109
- next: { command: vaultAccessCommand(undefined, options), reason: "创建一次账号读取授权" }, recovery: [],
111
+ instruction: `账号读取授权已缺失或过期。按当前动作或待填模板生成一次官方入口;用户完成后重新运行原始 read。${accessContextInstruction(options)}`,
112
+ ...vaultAccessGuidance(undefined, options), recovery: [],
110
113
  }, outputOptions(options));
111
114
  return;
112
115
  }
@@ -121,7 +124,7 @@ export function vaultAccessCommand(artifactRef, options) {
121
124
  const host = options.host ?? (openClaw ? "<host>" : undefined);
122
125
  if (host)
123
126
  parts.push("--host", host);
124
- const target = options.target ?? (openClaw && (!options.host || requiresTarget(options.host)) ? "<target>" : undefined);
127
+ const target = options.target ?? (((openClaw && !options.host) || (options.host && requiresTarget(options.host))) ? "<target>" : undefined);
125
128
  if (target)
126
129
  parts.push("--target", shellArgument(target));
127
130
  parts.push("--json");
@@ -132,10 +135,14 @@ export function accessContextInstruction(options) {
132
135
  ? " 将 <host> 和 <target> 替换为当前可信 OpenClaw 会话的真实值,不要照抄占位符或猜测目标。"
133
136
  : "";
134
137
  }
135
- function shellArgument(value) {
136
- if (value.startsWith("<") && value.endsWith(">"))
137
- return value;
138
- if (/^[\p{L}\p{N}._:=/-]+$/u.test(value))
139
- return value;
140
- return `'${value.replaceAll("'", `'"'"'`)}'`;
138
+ function contextFlags(options) {
139
+ return `${options.host ? ` --host ${options.host}` : ""}${options.target ? ` --target ${shellArgument(options.target)}` : ""}`;
140
+ }
141
+ function vaultReadCommand(artifact, sections, options) {
142
+ return `itpay vault read --artifact ${shellArgument(artifact)}${sections.map(section => ` --section ${shellArgument(section)}`).join("")}${contextFlags(options)} --json`;
143
+ }
144
+ export function vaultAccessGuidance(artifact, options) {
145
+ const command = vaultAccessCommand(artifact, options);
146
+ const missing = [...(options.agentType === "openclaw" && !options.host ? ["host"] : []), ...(options.host && requiresTarget(options.host) && !options.target || options.agentType === "openclaw" && !options.host && !options.target ? ["target"] : [])];
147
+ return missing.length ? { next: null, interaction: { schema_version: "itpay.interaction.v1", stage: "input_required", input_template: { command, required_input: missing, executable: false } } } : { next: { command, reason: "创建一次官方只读授权入口;用户完成后恢复原reader" } };
141
148
  }
package/dist/src/main.js CHANGED
@@ -25,7 +25,7 @@ import { runCancelRefund, runGetRefund, runListRefunds, runRefund, runWatchRefun
25
25
  import { runCartAdd, runCartAddQuoteServer, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShow, runCartShowServer, } from "./commands/cart.js";
26
26
  import { CommandContractError, errorRecoveryActions, printErrorRecovery, writeCommandEnvelope } from "./commands/guidance.js";
27
27
  import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
28
- import { runInstall } from "./commands/install.js";
28
+ import { INSTALL_AGENT_TYPES, runInstall } from "./commands/install.js";
29
29
  import { runSkillShow } from "./commands/skill.js";
30
30
  import { runNext } from "./commands/next.js";
31
31
  import { runVaultAccess, runVaultList, runVaultRead } from "./commands/vault.js";
@@ -139,17 +139,12 @@ function reportCLIError(error, contract) {
139
139
  const providerContractMismatch = error instanceof HttpError && error.code === "provider_contract_mismatch";
140
140
  const capabilityInputInvalid = error instanceof HttpError && error.code === "capability_input_invalid";
141
141
  const deviceKeyResettable = Boolean(deviceError && (deviceError.code === "agent_device_key_rotated" ||
142
- deviceError.code === "agent_device_key_conflict" ||
143
- (deviceError.enrollmentFailed && deviceError.status === 500)));
142
+ deviceError.code === "agent_device_key_conflict"));
144
143
  const deviceRecovery = deviceError ? [
145
144
  {
146
145
  command: "itpay skill show itpay --json",
147
146
  reason: "读取 ItPay 身份边界;该错误需要用户或运营恢复 Backend 登记,不能通过换类型或删除本地身份绕过",
148
147
  },
149
- ...(deviceKeyResettable ? [{
150
- command: "itpay device reset-key --confirm-key-reset --json",
151
- reason: "服务端拒绝以当前私钥完成设备登记;生成全新 Ed25519 密钥并重新登记(旧设备身份在服务端保留为孤儿,不影响新身份和额度谱系)",
152
- }] : []),
153
148
  ] : [];
154
149
  const stateRecovery = stateError ? [{
155
150
  command: "itpay skill show itpay --json",
@@ -167,10 +162,10 @@ function reportCLIError(error, contract) {
167
162
  ? "CLI 已自动续期并重试同一请求一次,仍被拒绝;停止重试,不要切换 Agent Type 或旋转身份。"
168
163
  : deviceError?.code === "agent_device_revoked"
169
164
  ? "Backend 已撤销当前 Device 登记;CLI 没有自动创建替代身份。停止重试并请用户或运营恢复登记。"
170
- : deviceKeyResettable && deviceError?.status === 500
171
- ? "服务端未能完成设备登记。先原样重试一次原命令:服务端会把已登记的同一公钥幂等挂回原设备。若仍返回 internal_error(Backend 未含该修复),执行 itpay device reset-key --confirm-key-reset 生成全新密钥后重试。"
165
+ : deviceError?.enrollmentFailed && deviceError.status === 500
166
+ ? "服务端未能完成设备登记;保持原密钥和Agent Type,说明系统故障,待服务端恢复后继续原任务。不要重置身份。"
172
167
  : deviceKeyResettable
173
- ? "服务端记录显示当前设备私钥已不再有效(已轮换或与既有登记冲突)。执行 itpay device reset-key --confirm-key-reset 生成全新密钥并重新登记。"
168
+ ? "服务端拒绝当前设备私钥登记(轮换或冲突)。停止自动恢复;换钥会放弃旧身份,旧执行访问不会迁移。仅用户或运营明确决定放弃旧身份后,按 device reset-key 帮助执行。"
174
169
  : deviceError
175
170
  ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
176
171
  : undefined;
@@ -226,9 +221,10 @@ function reportCLIError(error, contract) {
226
221
  ? "临时网络故障;CLI 已仅对可安全重放的操作完成有限自动重试,但仍未获得完整响应。按 recovery 查询同一资源的权威状态;不要创建替代 Checkout、Execution、Payment 或 Refund。"
227
222
  : "网络在完整响应前中断;当前写操作没有安全重放合同,因此 CLI 未自动重试。按 recovery 查询权威状态;不要原样重放或创建替代 Checkout、Execution、Payment 或 Refund。"
228
223
  : backendOverrideError
229
- ? "移除 ITPAY_BACKEND_URL 使用正式环境,或准确设置为 https://sandbox.itpay.ai。"
224
+ ? "移除 ITPAY_BACKEND_URL 使用正式环境,或按当前测试目标准确设置为 https://sandbox.itpay.ai 或 https://dev.itpay.ai。不要通过切换环境规避当前错误。"
230
225
  : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
231
226
  next: null,
227
+ ...(commandError?.interaction ? { interaction: commandError.interaction } : {}),
232
228
  recovery: incompatible
233
229
  ? requiredCLIVersion
234
230
  ? [distributionUpgradeAction(requiredCLIVersion)]
@@ -238,7 +234,7 @@ function reportCLIError(error, contract) {
238
234
  : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : lockError ? lockRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
239
235
  }, {
240
236
  ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError || lockError || stateError ? { jsonOutput: process.argv.includes("--json") } : {}),
241
- output: (text) => { process.stderr.write(text); },
237
+ output: (text) => { process.stdout.write(text); },
242
238
  });
243
239
  process.exitCode = 1;
244
240
  return;
@@ -323,7 +319,7 @@ for (const action of ["login", "status"]) {
323
319
  // and non-JSON output is human-readable instead of a raw JSON dump.
324
320
  const envelope = {
325
321
  status: String(result.status ?? "error"),
326
- result: (result.result ?? result),
322
+ result: (result.result ?? Object.fromEntries(Object.entries(result).filter(([key]) => !["status", "instruction", "next", "recovery", "handoff"].includes(key)))),
327
323
  ...(result.handoff ? { handoff: result.handoff } : {}),
328
324
  instruction: typeof result.instruction === "string" ? result.instruction : "按返回状态继续。",
329
325
  next: (result.next ?? null),
@@ -422,8 +418,11 @@ deviceCmd
422
418
  .action(async (options) => {
423
419
  const config = loadConfig();
424
420
  try {
421
+ if (!config.agentType || !INSTALL_AGENT_TYPES.some(type => type === config.agentType)) {
422
+ throw new CommandContractError("agent_type_required", "supported Agent Type required before key reset", "先按真实宿主配置正式支持的Agent Type;尚未修改本地密钥。", [{ command: "itpay install --json", reason: "读取当前宿主配置" }]);
423
+ }
425
424
  if (!options.confirmKeyReset) {
426
- throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅当服务端拒绝以当前私钥完成设备登记(internal_error、agent_device_key_rotated 或 agent_device_key_conflict)时使用;会放弃本地设备身份并重新登记,旧设备在服务端保留为孤儿。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
425
+ throw new CommandContractError("key_reset_confirmation_required", "--confirm-key-reset is required", "仅在用户或运营明确决定放弃旧身份后使用;会丢弃本地私钥,旧执行访问不会迁移。internal_error 需服务端恢复,不能换钥。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
427
426
  }
428
427
  const reset = await new DeviceAuthority({
429
428
  baseURL: config.baseURL,
@@ -437,9 +436,9 @@ deviceCmd
437
436
  private_key_preserved: false,
438
437
  server_side_device: "orphaned_under_previous_key",
439
438
  },
440
- instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记并获得新的额度谱系。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
439
+ instruction: "本地设备私钥已重置;下一次需要设备身份的命令会以全新 Ed25519 密钥重新登记;这不能恢复旧身份的订单或用于刷新额度。服务端旧设备记录保留为孤儿;如需清理请走运营流程。",
441
440
  next: {
442
- command: `itpay --agent-type ${config.agentType ?? "<agent_type>"} services list --limit 1 --json`,
441
+ command: `itpay --agent-type ${config.agentType} services list --limit 1 --json`,
443
442
  reason: "用无业务写入的签名请求完成新密钥的重新登记",
444
443
  },
445
444
  recovery: [],
@@ -1413,7 +1412,7 @@ services
1413
1412
  .argument("<result_item_id>")
1414
1413
  .option("--offset <offset>", "zero-based candidate offset", Number, 0)
1415
1414
  .option("--cursor <cursor>", "opaque v2 page cursor (rcur_<offset>); takes precedence over --offset")
1416
- .option("--limit <limit>", "page size (1-20)", Number, 5)
1415
+ .option("--limit <limit>", "page size (1-20)", Number, 20)
1417
1416
  .option("--json", "output JSON")
1418
1417
  .action(async (serviceExecutionID, resultItemID, options) => {
1419
1418
  const config = loadConfig();
@@ -1477,12 +1476,18 @@ services
1477
1476
  .argument("<service_execution_id>")
1478
1477
  .requiredOption("--capability <capability_id>")
1479
1478
  .option("--input <key=value>", "redacted input summary", collectOption, [])
1479
+ .option("--input-json <file>", "JSON object containing the complete capability input")
1480
1480
  .option("--json", "output JSON")
1481
1481
  .action(async (serviceExecutionID, options) => {
1482
1482
  const config = loadConfig();
1483
1483
  const backend = newBackendClient(config);
1484
1484
  try {
1485
- await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
1485
+ const recovery = [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "读取当前动作要求" }];
1486
+ if (options.inputJson && options.input.length > 0) {
1487
+ throw new CommandContractError("capability_input_invalid", "--input and --input-json cannot be combined", "嵌套输入用 --input-json <file>,扁平输入用 --input key=value,二选一;本次未调用供应商。", recovery);
1488
+ }
1489
+ const input = options.inputJson ? readInputJsonObject(options.inputJson, "capability_input_invalid", recovery) : parseKeyValueList(options.input);
1490
+ await runServicesInvoke(backend, config, serviceExecutionID, options.capability, input, { jsonOutput: Boolean(options.json) });
1486
1491
  }
1487
1492
  catch (error) {
1488
1493
  reportCLIError(error, {
@@ -1665,12 +1670,16 @@ services
1665
1670
  .description("Show the next recommended agent action for a Service Execution")
1666
1671
  .argument("<service_execution_id>")
1667
1672
  .option("--since-snapshot <snapshot_id>", "delta read: suppress the journey payload when unchanged")
1673
+ .option("--timeout <seconds>", "wait up to 120 seconds for a useful result on this execution", (value) => Number(value))
1668
1674
  .option("--json", "output JSON instead of terminal text")
1669
1675
  .action(async (serviceExecutionID, options) => {
1670
1676
  const config = loadConfig();
1671
1677
  const backend = newBackendClient(config);
1672
1678
  try {
1673
- await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot });
1679
+ if (options.timeout !== undefined && (!Number.isInteger(options.timeout) || options.timeout < 0 || options.timeout > 120)) {
1680
+ throw new Error("--timeout must be an integer from 0 to 120");
1681
+ }
1682
+ await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), sinceSnapshot: options.sinceSnapshot, timeoutSeconds: options.timeout });
1674
1683
  }
1675
1684
  catch (error) {
1676
1685
  reportCLIError(error, {
@@ -1683,22 +1692,28 @@ services
1683
1692
  });
1684
1693
  services
1685
1694
  .command("read-result")
1686
- .description("Read a human-granted service result for this agent")
1695
+ .description("Read saved railway results or human-granted service content")
1687
1696
  .argument("<service_execution_id>")
1688
- .option("--snapshot <snapshot_id>", "rail.progressive.v2: read one journey from a committed planning snapshot")
1697
+ .option("--snapshot <snapshot_id>", "Smart: saved snapshot catalog; add --journey for a journey detail. Exact: saved station-pair rows without --snapshot")
1689
1698
  .option("--journey <journey_id>", "rail.progressive.v2: journey id to read")
1699
+ .option("--offset <offset>", "saved journey offset", Number, 0)
1700
+ .option("--limit <limit>", "rows per page (1-20): Exact 20, Smart 3", Number)
1701
+ .option("--all", "read every saved journey page")
1690
1702
  .option("--json", "output JSON instead of terminal text")
1691
1703
  .action(async (serviceExecutionID, options) => {
1692
1704
  const config = loadConfig();
1693
1705
  const backend = newBackendClient(config);
1694
1706
  try {
1695
- await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey });
1707
+ await runServicesReadResult(backend, serviceExecutionID, { jsonOutput: Boolean(options.json), snapshot: options.snapshot, journey: options.journey,
1708
+ offset: options.offset, limit: options.limit, all: Boolean(options.all) });
1696
1709
  }
1697
1710
  catch (error) {
1698
1711
  reportCLIError(error, {
1699
1712
  jsonOutput: Boolean(options.json),
1700
- code: "agent_access_denied",
1701
- instruction: "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。",
1713
+ code: error instanceof HttpError && error.code === "agent_access_denied" ? "agent_access_denied" : "service_result_read_failed",
1714
+ instruction: error instanceof HttpError && error.code === "agent_access_denied"
1715
+ ? "请用户在订单页面重新授权;不要使用开发者权限绕过授权或退款锁。"
1716
+ : "读取同一服务的已保存结果;若快照已不可读,先查看该服务当前状态和有效动作。",
1702
1717
  recovery: [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "检查交付模式和 grant 状态" }],
1703
1718
  });
1704
1719
  }
@@ -24,9 +24,6 @@ export function buildAgentChatHandoff(plan) {
24
24
  if (plan.orderItems?.length) {
25
25
  lines.push(`项目:${plan.orderItems.map((item) => `${item.title} x${item.quantity}`).join("、")}`);
26
26
  }
27
- if (plan.afterActionCommand) {
28
- lines.push("", `付款后查询:\`${plan.afterActionCommand}\``);
29
- }
30
27
  return {
31
28
  type: "send_payment_handoff",
32
29
  must_send_to_user: true,
@@ -59,11 +59,11 @@ const TERMINAL_SESSION_STATES = {
59
59
  },
60
60
  cancelled: {
61
61
  status: 'auth_cancelled',
62
- instruction: '用户在官方页取消了授权。如仍需登录,重新运行 itpay auth login。',
62
+ instruction: '用户在官方页取消了授权;停止登录。只有用户明确重新要求授权时才发起新的登录。',
63
63
  },
64
64
  denied: {
65
65
  status: 'auth_denied',
66
- instruction: '官方授权被拒绝。重新运行 itpay auth login 生成新请求。',
66
+ instruction: '官方授权被拒绝;停止登录,不自动创建新请求。只有用户明确重新要求授权时才重新登录。',
67
67
  },
68
68
  };
69
69
  class AuthRequestError extends Error {
@@ -200,9 +200,13 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
200
200
  }
201
201
  catch (error) {
202
202
  if (error instanceof AuthRequestError && (error.httpStatus === 404 || error.httpStatus === 410)) {
203
- return sessionGone(env, purpose, baseURL, open.sessionID);
203
+ rmSync(sellerAuthPath(baseURL, env, purpose), { force: true });
204
+ // The explicit login call may open one fresh session now. Status
205
+ // remains read-only and still reports the missing old session.
206
+ }
207
+ else {
208
+ return authStatusUnknown(open.sessionID);
204
209
  }
205
- return authStatusUnknown(open.sessionID);
206
210
  }
207
211
  }
208
212
  }
@@ -285,7 +289,7 @@ async function accountAuth(action, baseURL, env, fetcher, backend) {
285
289
  status: terminal.status,
286
290
  result: { dashboard_auth_session_id: state.sessionID, stage: progress.status },
287
291
  instruction: terminal.instruction,
288
- next: { command: 'itpay auth login --json', reason: '重新发起官方授权' },
292
+ next: ['denied', 'cancelled'].includes(progress.status) ? null : { command: 'itpay auth login --json', reason: '需要登录时重新发起官方授权' },
289
293
  recovery: [],
290
294
  };
291
295
  }
@@ -55,9 +55,12 @@ export class CartSession {
55
55
  ...(this.state.lastDisplayToken ? { lastDisplayToken: this.state.lastDisplayToken } : {}),
56
56
  ...(this.state.lastCheckoutURL ? { lastCheckoutURL: this.state.lastCheckoutURL } : {}),
57
57
  };
58
+ const contents = JSON.stringify(toSave, null, 2);
59
+ if (existsSync(path) && readFileSync(path, "utf-8") === contents)
60
+ return;
58
61
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
59
62
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
60
- writeFileSync(temporary, JSON.stringify(toSave, null, 2), { encoding: "utf-8", mode: 0o600 });
63
+ writeFileSync(temporary, contents, { encoding: "utf-8", mode: 0o600 });
61
64
  chmodSync(temporary, 0o600);
62
65
  renameSync(temporary, path);
63
66
  chmodSync(path, 0o600);
@@ -15,7 +15,7 @@ import { OperationJournal } from "./operation_journal.js";
15
15
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
16
16
  export const DEV_BASE_URL = "https://dev.itpay.ai";
17
17
  export const SANDBOX_BASE_URL = "https://sandbox.itpay.ai";
18
- export const CLI_VERSION = "2.1.19";
18
+ export const CLI_VERSION = "2.1.29";
19
19
  export const API_CONTRACT_REVISION = "sha256:2f9fbcdc5cd0630910abb52671304cc019e348e9a778017ec92ddc3513b3639f";
20
20
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
21
21
  const CART_SESSION_FILENAME = "cart.json";
@@ -45,6 +45,8 @@ export class TaskJournal {
45
45
  observe(serviceExecutionID, stage, resumeCommand) {
46
46
  const file = this.readFile();
47
47
  const existing = file.tasks[serviceExecutionID];
48
+ if (existing && existing.stage === stage && (resumeCommand === undefined || existing.resume_command === resumeCommand))
49
+ return;
48
50
  file.tasks[serviceExecutionID] = {
49
51
  service_execution_id: serviceExecutionID,
50
52
  ...(existing?.service_id ? { service_id: existing.service_id } : {}),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "cart-checkout",
6
6
  "title": "Canonical Cart And Checkout Routing",
7
7
  "purpose": "Use the canonical server Cart and route service-backed lines through Service Execution without hardcoded service logic.",
@@ -32,11 +32,12 @@
32
32
  }
33
33
  ],
34
34
  "agent_rules": [
35
- "The Backend Cart is business truth; ~/.itpay-v3/cart.json only stores local recovery handles and explicit drafts.",
35
+ "The Backend Cart is business truth; the current backend-isolated local cart state file only stores local recovery handles and explicit drafts.",
36
36
  "Run the exact next.command returned by the CLI. Do not extract or invent a capability from a raw list.",
37
- "One service intent uses one Service Execution. Start another execution for another independent intent.",
37
+ "Railway query and booking use distinct Service Executions for one purchase journey. Start another execution for an independent intent.",
38
38
  "Ask for email only when the selected capability requires protected delivery; never invent contact data.",
39
- "A lost Checkout response is retried with the same Cart and persisted idempotency operation."
39
+ "A lost Checkout response is retried with the same Cart and persisted idempotency operation.",
40
+ "A single railway service checkout normally follows its booking execution; Cart is used only when the returned flow calls for it."
40
41
  ],
41
42
  "forbidden": [
42
43
  "Do not call ordinary buy for a service-backed Cart line.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "catalog-list",
6
6
  "title": "Browse ItPay V3 Service Catalog",
7
7
  "purpose": "Explain available services, outcomes, conditions, and prices in human language without exposing catalog identifiers.",
@@ -20,7 +20,8 @@
20
20
  "item_format": "Explain the title, what the human receives, any auxiliary discovery step, and the published price.",
21
21
  "service_flow": "Explain discovery as an auxiliary identity-confirmation step, including free quota and paid continuation. Present primary_service as the actual paid outcome. Read all values from the manifest.",
22
22
  "variant_selection": "Internal IDs are for the Agent's next command only and are never shown or requested from the human.",
23
- "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically."
23
+ "price_note": "amount_minor is in minor units (e.g., 10000 = 100.00 CNY). The CLI formats prices as XX.YY currency automatically.",
24
+ "railway": "Railway entries point to itpay docs show rail-booking; use its query and booking contracts instead of a generic start→invoke→cart sequence."
24
25
  },
25
26
  "agent_rules": [
26
27
  "Use catalog list only when the human asks for a new service, not when they want an earlier purchase.",
@@ -28,15 +29,32 @@
28
29
  "Explain the customer journey from service_flow instead of presenting every variant as an unrelated service.",
29
30
  "State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
30
31
  "Ask the human to choose only when multiple services could satisfy the goal; never make them choose an internal variant or offer ID.",
31
- "After services start, submit service keywords only through the returned capability command's --input key=value options; --target is never a search keyword."
32
+ "Use each service’s returned input contract; railway planning and booking follow rail-booking, not a universal Cart route."
32
33
  ],
33
34
  "human_language": {
34
- "prefer": ["服务", "查询", "报告", "免费次数", "价格", "你会得到什么"],
35
- "avoid": ["service_id", "variant_id", "offer_id", "capability", "manifest"]
35
+ "prefer": [
36
+ "服务",
37
+ "查询",
38
+ "报告",
39
+ "免费次数",
40
+ "价格",
41
+ "你会得到什么"
42
+ ],
43
+ "avoid": [
44
+ "service_id",
45
+ "variant_id",
46
+ "offer_id",
47
+ "capability",
48
+ "manifest"
49
+ ]
36
50
  },
37
51
  "next_docs": [
38
52
  {
39
- "condition": "Buyer selected a service and wants to purchase",
53
+ "condition": "Railway service selected",
54
+ "topic": "rail-booking"
55
+ },
56
+ {
57
+ "condition": "Non-rail Buyer selected a service and wants to purchase",
40
58
  "topic": "cart-checkout",
41
59
  "file": "docs/agent/buyer/cart-checkout.json"
42
60
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "itp.agent_doc.v1",
3
3
  "role": "buyer",
4
- "product_scope": "itpay is the single public CLI entry point, and $itpay is its user-facing Skill invocation. Under that one product entry point, the two top-level commerce actions are buy and sell: Buyer workflows are available now; Seller workflows will use the same entry point and are not implemented yet.",
4
+ "product_scope": "itpay is the single public CLI entry point for Buyer services and the existing itpay sell Seller workflow; $itpay is its user-facing Skill invocation.",
5
5
  "topic": "identity-and-sessions",
6
6
  "title": "Device Identity And Session Recovery",
7
7
  "purpose": "Explain stable local identity, official Backend-scoped registrations, Agent Type instances, and bounded automatic session recovery.",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "identity_model": {
14
14
  "local_key": "One owner-only Ed25519 private key for the local installation.",
15
- "backend_registration": "One registration per official Backend: app.itpay.ai by default and sandbox.itpay.ai only with the exact test override.",
15
+ "backend_registration": "One registration per configured official Backend. Follow the current CLI backend allowlist; do not switch to evade failures.",
16
16
  "agent_instance": "One instance per agent_type under each Backend registration; same-type windows and chats reuse it.",
17
17
  "presentation": "Host and target route output only and never identify the Agent or provide business input."
18
18
  },
@@ -23,13 +23,13 @@
23
23
  "When a previously unseen Agent Type is registered under an existing Device, the CLI skips locally known Agent Instances that the Backend reports as revoked and uses the first remaining valid Instance. It never reactivates a revoked Instance or replaces the Device; if none is valid, registration stops.",
24
24
  "When the Backend reports agent_device_not_found (HTTP 404) for a locally known registration — for example after the Backend registration database was rebuilt — the CLI drops only that Backend's registration and re-enrolls once with the same private key, re-attaching to the same device and quota lineage where the Backend still holds the key.",
25
25
  "A revoked v2 registration requires explicit operator recovery; it is never silently replaced.",
26
- "Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. If enrollment verification keeps failing with internal_error, or returns agent_device_key_rotated or agent_device_key_conflict, `itpay device reset-key --confirm-key-reset` is the supported way to start over with a fresh key."
26
+ "Re-enrolling a public key the Backend already holds re-attaches to the existing device and quota lineage after proof-of-possession; it never creates a duplicate device. An internal_error is a server failure: preserve the key and wait for service recovery. If enrollment returns agent_device_key_rotated or agent_device_key_conflict, stop automatic recovery. Only an explicit human/operator decision to abandon the old identity permits reset-key; old execution access does not migrate."
27
27
  ],
28
28
  "local_state_recovery": [
29
- "Device state updates use an atomic directory lock and owner-only atomic file replacement; no lock file is opened with fs.open or fs.openSync.",
29
+ "Device state uses an owner-only atomic file replacement and a lock that the CLI can inspect. On a stale lock, run the returned device repair-lock command; it only reclaims a proven stale owner.",
30
30
  "Operation idempotency uses one immutable owner-only record per operation key. Different commands and Agent processes do not share a global operation-journal lock; legacy operations.json is read only.",
31
31
  "On device_state_unwritable, use the reported operation and errno to diagnose the failing stage without exposing local paths; keep the same Agent Type and CLI/Node launcher, restore Host-approved persistent access to ~/.itpay-v3, then retry the original command.",
32
- "Do not manually create or delete state locks, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
32
+ "Do not delete a live lock, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
33
33
  ],
34
34
  "commands": [
35
35
  {
@@ -48,22 +48,22 @@
48
48
  "success_signal": "only the selected Backend registration is removed and the private key and other Backend registration remain"
49
49
  },
50
50
  {
51
- "intent": "abandon the local device key after the Backend refuses to complete enrollment with it (persistent internal_error, agent_device_key_rotated, or agent_device_key_conflict)",
52
- "command": "itpay device reset-key --confirm-key-reset --json",
53
- "success_signal": "the local private key is discarded and the next command enrolls as a brand-new device"
51
+ "intent": "repair only a proven stale local device lock",
52
+ "command": "itpay device repair-lock --json",
53
+ "success_signal": "the CLI reports whether the lock was stale and reclaimed; an active owner stays protected"
54
54
  }
55
55
  ],
56
56
  "agent_rules": [
57
57
  "Use the real runtime type and keep it stable across every command in the flow.",
58
58
  "Keep the same CLI/Node launcher and Host-approved permission context for the whole flow.",
59
59
  "Treat a new window, chat, task, or process of the same runtime as the same Agent Type, not a new identity.",
60
- "Expect production traffic to use https://app.itpay.ai. Use https://sandbox.itpay.ai only for an explicit test and keep the complete returned Backend-prefixed command for that entire flow.",
60
+ "Production defaults to https://app.itpay.ai; explicit sandbox or dev testing uses https://sandbox.itpay.ai or https://dev.itpay.ai. Keep the complete returned Backend-prefixed command for that flow.",
61
61
  "After automatic recovery fails, stop and report the exact code, selected official Backend, Agent Type, and command family without exposing private keys or tokens."
62
62
  ],
63
63
  "forbidden": [
64
64
  "Do not delete ~/.itpay-v3, rotate the key, switch Agent Type, or repeatedly retry to obtain fresh quota.",
65
65
  "Do not use device recover for session expiry, revocation, quota recovery, or an unconfirmed Backend failure.",
66
- "Do not redirect the CLI to dev, local, an IP, or any custom Backend; only the exact official sandbox.itpay.ai test override is allowed.",
66
+ "Do not switch Backend to evade a failure. Allowed public URLs are app.itpay.ai, sandbox.itpay.ai and dev.itpay.ai; local loopback requires the explicit CLI development flag.",
67
67
  "Do not use --target as identity or service input."
68
68
  ],
69
69
  "next_docs": [
@@ -93,10 +93,11 @@
93
93
  ],
94
94
  "auth_session_states": {
95
95
  "stages": "The CLI now reports the auth session stage exactly as the server returns it: waiting_provider (show the handoff and wait), email_verification_required (the human must verify the emailed code on the auth page), merge_confirmation_required (an existing account matches — the human must confirm the merge on the auth page), provider_pending/pending (generic wait), completed (session done — bind it, do not restart).",
96
- "terminal_states": "auth_expired/auth_denied/auth_cancelled/auth_failed mean this session is over; say so plainly and start a NEW session only if the human still wants in — never keep polling a dead one.",
97
- "transport_unknown": "auth_session_unknown means a network error while polling — the outcome is unknown. Keep waiting on the SAME session (it may still complete); do NOT restart auth, and do not assume denial.",
98
- "missing": "auth_session_not_found means the cached session is gone server-side; start a new login.",
99
- "completed_resume": "When login completes, itpay auth login resumes the remembered pending service execution automatically when one exists — you do not need to find it yourself.",
96
+ "terminal_states": "auth_expired/auth_denied/auth_cancelled/auth_failed mean this session is over; say so plainly and respect denial or cancellation. Start a new session only on an explicit login request; never keep polling a dead one.",
97
+ "transport_unknown": "auth_status_unknown means a network error while polling — the outcome is unknown. Keep waiting on the SAME session (it may still complete); do NOT restart auth, and do not assume denial.",
98
+ "missing": "auth_session_missing means the cached session is gone. An explicit auth login opens one fresh official session in that call; auth status stays read-only.",
99
+ "completed_resume": "After login completes, execute the returned command to resume the same paused service execution. Login does not silently run that command.",
100
100
  "path_b": "A buyer who reaches checkout without a login completes identity inside the protected checkout page; never force a separate itpay auth login first."
101
- }
101
+ },
102
+ "key_reset_decision": "Only after explicit user/operator acceptance of abandoning the old identity, consult device reset-key help; this is not automatic error recovery, and old execution access does not migrate."
102
103
  }
@@ -35,20 +35,21 @@
35
35
  "For first setup or a runtime change, identify the real Agent Type once, run itpay install <agent_type> --json yourself, and follow its setup continuation until the packaged Skill is loaded. Ordinary service requests start from the Skill's service command, without repeating install or readyz.",
36
36
  "After setup, tell the human only that ItPay is ready; never ask them to run setup commands or explain technical setup steps.",
37
37
  "Use the CLI distribution already provided by the current npm package, Skill, or plugin; update only through that distribution's own instructions.",
38
- "Production defaults to https://app.itpay.ai. Testing may set ITPAY_BACKEND_URL to the exact official URL https://sandbox.itpay.ai; no other Backend is allowed.",
38
+ "Production defaults to https://app.itpay.ai. Testing may set ITPAY_BACKEND_URL to the exact official URL the configured official test Backend only when explicitly requested; follow the CLI allowlist.",
39
39
  "If backend_contract_incompatible includes result.required_cli_version, stop all ItPay business commands and use only the exact distribution-specific update recovery returned by the CLI; never replace its version with latest.",
40
40
  "After upgrading, require itpay --version to equal result.required_cli_version before running readyz again; never change Agent Type or Device identity to recover compatibility.",
41
41
  "Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, workbuddy, zcode, doubao-work, kimi-code, or openclaw.",
42
42
  "For compatibility, a global --agent-type codex declaration is normalized immediately to codex-desktop; all registration, output, and returned commands use codex-desktop. Never generate the alias, and do not use codex as an install target.",
43
- "One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai and sandbox.itpay.ai.",
43
+ "One local private key is reused, while Device registrations and quota lineage remain separate for app.itpay.ai, sandbox.itpay.ai and dev.itpay.ai.",
44
44
  "Within each official Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
45
45
  "Agent Type identifies the runtime. Host and target only select a presentation destination; load render-hosts only when a handoff cannot be shown.",
46
46
  "Keep the exact Agent Type in every next or recovery command; do not fall back to a type previously used by another runtime.",
47
- "Do not change Agent Type or rotate local identity to reset quota or recover a failed command."
47
+ "Do not change Agent Type or rotate local identity to reset quota or recover a failed command.",
48
+ "Skillhub and local host bundles are separate installed artifacts; verify their version and packaged Skill/docs instead of assuming an npm update changed them."
48
49
  ],
49
50
  "forbidden": [
50
51
  "Do not use terminal, claude-code, or plain-chat as Agent Types. Do not generate codex as an Agent Type; it is accepted only as a legacy global declaration and becomes codex-desktop before any request.",
51
- "Do not set or suggest any Backend except the default https://app.itpay.ai or the exact test override https://sandbox.itpay.ai.",
52
+ "Do not set or suggest a Backend outside the CLI allowlist: https://app.itpay.ai, https://sandbox.itpay.ai, https://dev.itpay.ai; local loopback requires the CLI development flag. Do not switch Backend to evade a failure.",
52
53
  "Do not claim that install writes host configuration or registers a device; it only prints instructions.",
53
54
  "Do not create a new identity for a different window, task, chat, or process of the same Agent Type."
54
55
  ],