@itpay/cli 2.1.7 → 2.1.9

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.
@@ -73,6 +73,27 @@ export class DeviceAuthority {
73
73
  return { removed: true, agentTypes };
74
74
  });
75
75
  }
76
+ async resetDeviceKey() {
77
+ return withFileLock(`${this.statePath}.lock`, async () => {
78
+ const state = this.readState();
79
+ const removedBackends = state ? Object.keys(state.registrations).sort() : [];
80
+ // Delete the key first: if the process dies before the state write, the
81
+ // next run treats the missing key as a fresh install and completes the
82
+ // reset itself; it can never pair a new key with stale registrations.
83
+ if (existsSync(this.privateKeyPath)) {
84
+ try {
85
+ unlinkSync(this.privateKeyPath);
86
+ }
87
+ catch (error) {
88
+ if (error.code !== "ENOENT") {
89
+ throw asDeviceStateError(error, "remove_private_key") ?? error;
90
+ }
91
+ }
92
+ }
93
+ this.writeState(emptyDeviceState());
94
+ return { removedBackends };
95
+ });
96
+ }
76
97
  async prepareAuthorization() {
77
98
  let state = this.readState() ?? emptyDeviceState();
78
99
  const agentType = this.requestedAgentType;
@@ -83,8 +104,14 @@ export class DeviceAuthority {
83
104
  if (!privateKey) {
84
105
  const pair = generateKeyPairSync("ed25519");
85
106
  privateKey = pair.privateKey;
86
- this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
107
+ // Persist an empty state before the new key ever reaches disk so a crash
108
+ // can never pair the new key with registrations bound to the old one.
109
+ // A crash between the two writes leaves either no key (fresh retry) or a
110
+ // key with no registrations (the backend re-attaches to the existing
111
+ // device instead of creating a duplicate).
87
112
  state = emptyDeviceState();
113
+ this.writeState(state);
114
+ this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
88
115
  }
89
116
  let registration = state.registrations[this.backendKey];
90
117
  if (!registration && state.legacyRegistration) {
@@ -102,7 +129,21 @@ export class DeviceAuthority {
102
129
  registration = await this.enroll(agentType, privateKey);
103
130
  }
104
131
  state.registrations[this.backendKey] = registration;
105
- const session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
132
+ let session;
133
+ try {
134
+ session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
135
+ }
136
+ catch (error) {
137
+ if (!isUnknownDeviceRegistration(error))
138
+ throw error;
139
+ // The backend lost this registration (e.g. its identity store was
140
+ // rebuilt). Drop the stale record and enroll once: the same key either
141
+ // re-attaches to the surviving device or creates a fresh one.
142
+ delete state.registrations[this.backendKey];
143
+ registration = await this.enroll(agentType, privateKey);
144
+ state.registrations[this.backendKey] = registration;
145
+ session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
146
+ }
106
147
  this.writeState(state);
107
148
  return { state: registration, agentType, session, privateKey };
108
149
  }
@@ -136,16 +177,23 @@ export class DeviceAuthority {
136
177
  if (!publicJWK.x)
137
178
  throw new Error("unable to export Ed25519 public key");
138
179
  const publicKey = Buffer.from(publicJWK.x, "base64url").toString("base64");
139
- const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
140
- const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
141
- const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
142
- return {
143
- deviceID: verified.agent_device_id,
144
- deviceKeyID: verified.agent_device_key_id,
145
- quotaLineageID: verified.quota_lineage_id,
146
- agentInstances: { [verified.agent_type]: verified.agent_instance_id },
147
- sessions: {},
148
- };
180
+ try {
181
+ const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
182
+ const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
183
+ const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
184
+ return {
185
+ deviceID: verified.agent_device_id,
186
+ deviceKeyID: verified.agent_device_key_id,
187
+ quotaLineageID: verified.quota_lineage_id,
188
+ agentInstances: { [verified.agent_type]: verified.agent_instance_id },
189
+ sessions: {},
190
+ };
191
+ }
192
+ catch (error) {
193
+ if (error instanceof DeviceAuthorizationError)
194
+ error.enrollmentFailed = true;
195
+ throw error;
196
+ }
149
197
  }
150
198
  async ensureSession(state, agentType, privateKey, force = false) {
151
199
  const existing = state.sessions[agentType];
@@ -233,6 +281,7 @@ export class DeviceAuthority {
233
281
  export class DeviceAuthorizationError extends Error {
234
282
  status;
235
283
  code;
284
+ enrollmentFailed = false;
236
285
  constructor(status, code, message) {
237
286
  super(message);
238
287
  this.status = status;
@@ -257,6 +306,9 @@ function emptyDeviceState() {
257
306
  function canMovePastLegacyRegistration(error) {
258
307
  return error instanceof DeviceAuthorizationError && (error.code === "agent_device_revoked" || error.status === 404);
259
308
  }
309
+ function isUnknownDeviceRegistration(error) {
310
+ return error instanceof DeviceAuthorizationError && (error.code === "agent_device_not_found" || error.status === 404);
311
+ }
260
312
  function normalizeBackendKey(value) {
261
313
  const url = new URL(value);
262
314
  url.search = "";
@@ -7,7 +7,7 @@
7
7
  "purpose": "Explain stable local identity, official Backend-scoped registrations, Agent Type instances, and bounded automatic session recovery.",
8
8
  "when_to_use": [
9
9
  "A different Agent window or runtime starts using an existing CLI installation.",
10
- "The CLI reports device_state_unwritable, or the Backend reports agent_identity_required, agent_device_session_required, or agent_device_revoked.",
10
+ "The CLI reports device_state_unwritable, or the Backend reports agent_identity_required, agent_device_session_required, agent_device_not_found, or agent_device_revoked.",
11
11
  "An operator confirms that the currently selected official Backend registration database was reset."
12
12
  ],
13
13
  "identity_model": {
@@ -21,7 +21,9 @@
21
21
  "On one HTTP 401 with code agent_device_session_required, it invalidates only the current Backend and Agent Type session, renews it, and retries the exact request once.",
22
22
  "A second failure is returned to the Agent. The CLI does not loop, switch Agent Type, rotate the key, or create a replacement v2 device.",
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
- "A revoked v2 registration requires explicit operator recovery; it is never silently replaced."
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
+ "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."
25
27
  ],
26
28
  "local_state_recovery": [
27
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.",
@@ -44,6 +46,11 @@
44
46
  "intent": "recover after an operator-confirmed reset of the selected official Backend registration database",
45
47
  "command": "itpay --agent-type <agent_type> device recover --confirm-backend-reset --json",
46
48
  "success_signal": "only the selected Backend registration is removed and the private key and other Backend registration remain"
49
+ },
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"
47
54
  }
48
55
  ],
49
56
  "agent_rules": [
@@ -1,19 +1,21 @@
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, 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 use the same entry point via itpay sell.",
5
5
  "topic": "rail-booking",
6
6
  "title": "Railway Query, Planning, and Ticket Booking",
7
- "purpose": "Choose the right railway service for the buyer's situation, submit the exact input contract each service expects, and follow the protected booking flow without inventing field names.",
7
+ "purpose": "Choose the right railway service for the buyer's situation, submit the exact input contract each service expects, bridge a chosen train into booking via the server-issued selection, and follow the protected checkout flow without inventing field names.",
8
8
  "when_to_use": [
9
9
  "The buyer wants train schedules, routes, or tickets.",
10
10
  "A railway service execution fails at the geo, search, catalog, or recommend step.",
11
11
  "The buyer only knows a city or fuzzy location rather than a station name.",
12
- "The buyer asks about seat preferences, refunds, or ticket issuance status."
12
+ "The buyer asks about seat preferences, refunds, or ticket issuance status.",
13
+ "A query returned login_required or rate_limited."
13
14
  ],
14
15
  "service_selection": {
15
16
  "itpay-rail-exact": "Use when the buyer knows BOTH the exact departure and arrival railway station names (e.g. 古镇 → 广州南). Direct train query, fastest path. Do NOT use it with only a city or fuzzy location.",
16
17
  "itpay-rail-smart": "Use when the buyer has a city name, an area, an address, or wants transfer planning and recommendations. The platform resolves locations to stations itself; never silently convert a city into its namesake station.",
18
+ "itpay-rail-booking": "Purchase service — use ONLY after the buyer explicitly chose a train and seat from a query result. Starts quote → protected checkout → payment → asynchronous issuance on one execution. Never queried for schedules.",
17
19
  "svc_qizhidao_company_lookup": "Not a railway service; do not mix intents."
18
20
  },
19
21
  "input_contract": {
@@ -24,7 +26,11 @@
24
26
  "travel_date": "Travel date in YYYY-MM-DD. The field name must be exactly travel_date; aliases like date are rejected."
25
27
  },
26
28
  "optional": "None. additionalProperties is false — extra fields are rejected by the provider.",
27
- "example": {"origin": "古镇", "destination": "广州南", "travel_date": "2026-09-19"}
29
+ "example": {
30
+ "origin": "古镇",
31
+ "destination": "广州南",
32
+ "travel_date": "2026-09-19"
33
+ }
28
34
  },
29
35
  "itpay-rail-smart": {
30
36
  "required": {
@@ -42,14 +48,48 @@
42
48
  "priority": "One of balanced | fastest | cheapest | safest | flexible.",
43
49
  "max_transfers": "Integer 0-2, allowed transfer count."
44
50
  },
45
- "example": {"origin": "中山古镇", "destination": "广州南", "travel_date": "2026-09-19", "priority": "fastest"}
51
+ "example": {
52
+ "origin": "中山古镇",
53
+ "destination": "广州南",
54
+ "travel_date": "2026-09-19",
55
+ "priority": "fastest"
56
+ }
57
+ },
58
+ "itpay-rail-booking": {
59
+ "preferred": {
60
+ "passengers": "Integer 1-5: number of passengers, NOT passenger identity records.",
61
+ "selection": {
62
+ "token": "The booking_offer.selection_token carried on the chosen result item.",
63
+ "seat_type": "Seat code when the user picked a specific class, e.g. O."
64
+ }
65
+ },
66
+ "explicit_legs": {
67
+ "passengers": "Integer 1-5.",
68
+ "legs": "Array of 1-3 legs, each: from_station_code, to_station_code, travel_date, train_code, departure_time (HH:MM), arrival_time (HH:MM), arrival_days (int 0-7), seat_type. Only use when no booking_offer is available; never guess these values."
69
+ },
70
+ "boundary": "Identity fields (names, ID numbers, phones) never belong in this input; they are collected on the protected checkout page."
46
71
  }
47
72
  },
73
+ "selection_flow": [
74
+ "Exact results carry per-train candidates; smart results carry journeys. Bookable options carry booking_offer with a server-signed selection_token.",
75
+ "The token binds the stored result snapshot and the chosen option. Submit it with passengers (and seat_type when the user picked a class); the server derives the authoritative legs and re-quotes live. Do NOT hand-build legs JSON from the visible result.",
76
+ "An expired or foreign token returns a clear error; recover by running a fresh query or by explicit legs — never by guessing token contents.",
77
+ "Repeating the same selection restores the same booking execution instead of creating a second one."
78
+ ],
79
+ "trial_and_login": {
80
+ "policy": "itpay-rail-exact and itpay-rail-smart each allow 2 anonymous queries per enrolled device lineage — independent counters, not a shared pool of 4.",
81
+ "login_required": "Trial for that service is exhausted. The query is paused BEFORE provider calls; the execution keeps its input. Offer itpay auth login; after binding, resume the SAME execution with services run --execution <id> (no input file). Do not start a new execution, do not make the user re-describe the trip, and do not burn the other service's quota to dodge the gate.",
82
+ "rate_limited": "Registered accounts have no cumulative search cap but normal per-minute limits apply; wait retry_after_ms and resume the same execution.",
83
+ "fourth_result": "The 4th result is delivered first; only the next new query requires authorization.",
84
+ "purchase_after_trial": "Selecting a saved option and buying it does not consume search quota and works even when trial is exhausted."
85
+ },
48
86
  "flow": [
49
87
  "services start <service> --json returns input_required plus the guidance block for rail services.",
50
88
  "Submit input with services run <service> --execution <id> --input-json <file> --json on the SAME execution.",
51
89
  "itpay-rail-smart may stop at a location confirmation step; show the candidate places to the buyer, then continue the same execution after the human confirms.",
52
- "After a train and seat are chosen, the booking flow moves to a protected checkout page: passenger identity data is entered by the human on that page, never through the CLI.",
90
+ "After the buyer explicitly picks a train/seat, run itpay-rail-booking with the selection token (preferred) or explicit legs. The execution then pauses at the booking review (workflow:confirm_booking): services next returns booking_review_required with human_action.context.review carrying draft_revision, legs, seat_options and notice_version.",
91
+ "Collect the review input and submit it with services action <id> --action workflow:confirm_booking --actor-type human --status approved --input-json <file> --json. Required fields: draft_revision (exact server value), passengers, seat_type, seat_preferences (one entry per passenger_index 0..n-1, values limited to that leg's seat_options), party_preference, fallback=automatic_assignment, notice_version, accept_non_guaranteed=true. Only after an accepted review does the server quote and offer checkout.",
92
+ "The booking execution reaches a protected checkout: passenger identity data is entered by the human on that page, never through the CLI. Account authentication may happen inside checkout — do not force a separate auth login first (Path B).",
53
93
  "Payment success does not mean the ticket is issued. Issuance is asynchronous; keep reading the same execution with services next.",
54
94
  "If seat details are still syncing after issuance, advise the buyer to verify the itinerary on 12306 instead of repeating the query."
55
95
  ],
@@ -58,23 +98,68 @@
58
98
  "geo": "Location resolution failed: check origin/destination are real place names, or attach origin_city/coordinate hints, then start a NEW execution (a failed execution is terminal).",
59
99
  "search": "Supplier query failed: most commonly a wrong field name (must be travel_date) or a nonexistent station name; recheck input against this doc.",
60
100
  "catalog_or_recommend": "Location resolved but no feasible trains: change the date or stations.",
61
- "general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service."
101
+ "login_required_is_not_failure": "login_required / rate_limited pause the execution; they are NOT failures and never justify a replacement execution.",
102
+ "general_rule": "Never replay a failed execution and never guess different field names; correct the input and start a new execution of the same service.",
103
+ "booking_review_changed": "The submitted draft_revision is stale: re-read services next for the current review projection and resubmit the full input; do not bump the revision yourself."
62
104
  },
63
105
  "seat_preferences": {
64
106
  "meaning": "A buyer's seat-letter preference may be planned per leg before payment. The plan is submitted to the supplier when encodable; unsupported combinations fall back to automatic assignment.",
65
107
  "letters": "Evidence-backed letters only: O=ABCDF (second class), M=ACDF (first class), 9=ACF (business class). Other layouts auto-assign.",
66
- "boundary": "Never claim a preference was fulfilled before supplier confirmation; check the issued seat in the order/status output."
108
+ "boundary": "Never claim a preference was fulfilled before supplier confirmation; check the issued seat in the order/status output.",
109
+ "review_contract": "Submitted inside the confirm_booking action via --input-json (nested JSON; --input key=value cannot express it and must not be combined with it). One preference per passenger slot, passenger_index 0-based. draft_revision must equal the server projection; a stale revision returns booking_review_changed — re-read services next and resubmit, never increment locally.",
110
+ "revision": "While payment has not started, the same execution accepts a revised review: resubmit confirm_booking with the new draft_revision; the old unpaid quote/checkout is retired and re-quoted. After a payment attempt the review is closed — never create a replacement purchase."
67
111
  },
68
112
  "agent_rules": [
69
- "Pick the service by the buyer's input precision: exact station names → itpay-rail-exact; anything fuzzier → itpay-rail-smart.",
113
+ "Pick the service by the buyer's input precision: exact station names → itpay-rail-exact; anything fuzzier → itpay-rail-smart; explicit purchase intent after a selection → itpay-rail-booking.",
70
114
  "Never invent input field names. If the guidance block or this doc does not list a field, do not send it.",
71
115
  "Never ask the buyer for passenger names, ID numbers, or phone numbers in chat; those are collected only on the protected checkout page.",
116
+ "Never reconstruct provider-shaped legs JSON when a booking_offer.selection_token is available; submit the token plus passengers.",
117
+ "Seat preferences are requests, not guarantees: always repeat the notice that automatic assignment remains possible, and never claim a specific seat before the issued order shows it.",
118
+ "Take draft_revision only from the current services next review projection; on booking_review_changed re-read instead of retrying the stale value.",
119
+ "login_required and rate_limited are pauses, not failures: resume the same execution, never spawn a replacement.",
72
120
  "Do not repeat or replay a failed execution; correct the input and start a new one.",
73
121
  "Do not claim payment equals ticket issuance; issuance is asynchronous and confirmed only by the issued seat status."
74
122
  ],
75
123
  "human_language": {
76
- "prefer": ["车次", "出发站", "到达站", "出行日期", "座位", "余票", "出票中", "已出票"],
77
- "avoid": ["workflow", "execution", "provider", "seat_source", "candidate_id"]
124
+ "prefer": [
125
+ "车次",
126
+ "出发站",
127
+ "到达站",
128
+ "出行日期",
129
+ "座位",
130
+ "余票",
131
+ "出票中",
132
+ "已出票",
133
+ "登录后继续免费查询"
134
+ ],
135
+ "avoid": [
136
+ "workflow",
137
+ "execution",
138
+ "provider",
139
+ "seat_source",
140
+ "candidate_id",
141
+ "token",
142
+ "quota"
143
+ ]
78
144
  },
79
- "search_terms": ["rail", "railway", "train", "ticket", "火车", "火车票", "高铁", "车票", "车次", "seat", "booking", "rail-booking"]
145
+ "search_terms": [
146
+ "rail",
147
+ "railway",
148
+ "train",
149
+ "ticket",
150
+ "火车",
151
+ "火车票",
152
+ "高铁",
153
+ "车票",
154
+ "车次",
155
+ "seat",
156
+ "booking",
157
+ "rail-booking"
158
+ ],
159
+ "notifications": {
160
+ "channels": "Delivery notices reach the checkout-confirmed contact: email always works; a verified mainland SMS recipient is optional and checkout-scoped. The passenger phone, the login phone and the notification recipient are independent facts.",
161
+ "status_read": "The order read exposes masked per-leg notification entries (keys: issued, details_pending, seat_ready) with channel, status and masked recipient — never raw numbers.",
162
+ "truthfulness": "Provider acceptance is not delivery; an uncertain send outcome is reconciled, not silently retried. A notification failure never changes ticket or refund state; report it as a delivery detail, not an order failure.",
163
+ "unavailable": "If SMS is not configured or a recipient is unverified, email still satisfies the contact policy — never present an unverified phone as enabled."
164
+ }
80
165
  }
@@ -1,9 +1,94 @@
1
1
  # Account login
2
2
 
3
- Use `itpay auth login --json` to enroll the current Agent and open the official ItPay Web login. After the user finishes login and email verification, run `itpay auth status --json` to bind this enrolled Agent to the account. Keep declaring the actual `--agent-type` on both commands.
3
+ `itpay auth` binds the enrolled Agent Device to a real ItPay account through an official authorization handoff. It is used by Path A (trial exhausted → log in → keep querying) and any time a command returns `login_required`. Path B purchases do **not** require running `itpay auth login` first — account authentication can happen inside the protected checkout instead.
4
4
 
5
- For dev, keep `ITPAY_BACKEND_URL=https://dev.itpay.ai` on every command. Login state and device registration are isolated from production.
5
+ ## Syntax
6
6
 
7
- Railway exact and smart query services each allow two anonymous queries after enrollment. After login, queries remain free within the published per-minute limits. On `login_required`, finish login and start a new query. On `rate_limited`, wait until the next minute; do not clear device state or retry in a loop.
7
+ ```bash
8
+ itpay auth login [--json]
9
+ itpay auth status [--json]
10
+ ```
8
11
 
9
- The Agent receives no general account bearer token. This binding reuses the existing stable device-account ownership mechanism; switching account owners is not supported. Seller authoring continues to use `itpay sell auth` separately.
12
+ Both commands require an enrolled Agent Device and use the current `--agent-type` and Backend (`ITPAY_BACKEND_URL`). The Agent never receives an account bearer token; binding reuses the stable device-account ownership mechanism.
13
+
14
+ ## `itpay auth login`
15
+
16
+ Creates one official authorization request bound to this Backend, Device and Agent Instance, then stops. Repeating `auth login` while a valid request is open returns the same request — it does not rotate the QR code or create parallel sessions.
17
+
18
+ ```json
19
+ {
20
+ "status": "auth_pending",
21
+ "result": {
22
+ "dashboard_auth_session_id": "das_...",
23
+ "expires_at": "2026-09-20T12:34:56Z",
24
+ "methods": ["phone", "email", "alipay", "wechat"]
25
+ },
26
+ "handoff": {
27
+ "url": "https://dev.itpay.ai/signin?dashboard_auth_session_id=das_...&start_token=...",
28
+ "qr_local_path": "...",
29
+ "qr_image_url": "...",
30
+ "markdown": "..."
31
+ },
32
+ "instruction": "把官方授权页或二维码交给用户。用户在页面内选择手机号验证码、邮箱或钱包完成登录;不要替用户输入手机号或验证码。",
33
+ "next": {
34
+ "command": "itpay auth status --json",
35
+ "reason": "用户完成页面登录后确认绑定",
36
+ "poll_after_ms": 5000
37
+ },
38
+ "recovery": []
39
+ }
40
+ ```
41
+
42
+ - `handoff.url` is the official sign-in page carrying this authorization request. The page offers phone-code verification, email verification, and wallet options — the user chooses; the Agent must not deep-link a single wallet and call it universal authorization.
43
+ - The URL's `start_token` is valid only inside the official page handoff. Never log it, echo it in chat, or send it to another host.
44
+ - When the user already finished login before `auth login` runs, the command returns `authenticated` directly.
45
+
46
+ ## `itpay auth status`
47
+
48
+ Polls the open authorization request, then performs the device binding. Safe to run repeatedly and concurrently: only the requester Device/Instance can complete the binding.
49
+
50
+ ```json
51
+ {
52
+ "status": "auth_pending",
53
+ "result": { "dashboard_auth_session_id": "das_...", "expires_at": "..." },
54
+ "instruction": "用户仍在官方页面完成登录;保留当前 handoff,不要生成新二维码。",
55
+ "next": { "command": "itpay auth status --json", "poll_after_ms": 5000 },
56
+ "recovery": []
57
+ }
58
+ ```
59
+
60
+ ```json
61
+ {
62
+ "status": "authenticated",
63
+ "result": {
64
+ "buyer_id": "buyer_...",
65
+ "bound": true,
66
+ "query_quota": [
67
+ { "service_id": "itpay-rail-exact", "policy": "free_registered", "remaining": null },
68
+ { "service_id": "itpay-rail-smart", "policy": "free_registered", "remaining": null }
69
+ ]
70
+ },
71
+ "instruction": "登录与绑定已完成,告知用户可继续之前的查询;已注册账号查询不消耗试用次数,仍受正常限流。",
72
+ "next": { "command": "itpay services run <service_id> --execution <pending_execution_id> --json", "reason": "恢复被额度暂停的原查询" },
73
+ "recovery": []
74
+ }
75
+ ```
76
+
77
+ Terminal and error states:
78
+
79
+ | `status` | Meaning | Agent action |
80
+ | --- | --- | --- |
81
+ | `authenticated` | Device bound to a real account | Resume the original intent (a quota-blocked execution, the user's request) unchanged |
82
+ | `auth_pending` | User has not finished the page | Keep the same handoff; poll with `poll_after_ms` |
83
+ | `auth_expired` | Session expired before completion | Offer to start `itpay auth login` again; prior query input is still preserved server-side |
84
+ | `login_required` | No open request, not bound | Run `itpay auth login` |
85
+ | `auth_cancelled` / `auth_denied` | User declined or closed the page | Respect the refusal; do not silently re-open; the pending query stays resumable |
86
+
87
+ ## Rules
88
+
89
+ - The human's phone number and SMS code are entered only on the official page — never through chat or CLI input. A `buyer_id`, a typed phone string, or a passenger phone is **not** proof of phone verification.
90
+ - Verified email OR verified mainland phone both establish a valid login; the user picks on the page. Phone verification is not a prerequisite for registered free search — a verified email alone is sufficient.
91
+ - Login never grants Vault content, passenger history, or another Agent's authority. Other devices/platforms enroll and authorize separately.
92
+ - After `authenticated`, server-side identity is re-read; an anonymous execution blocked at the quota step resumes with `services run <service> --execution <id> --json` — no new execution, no re-asked itinerary.
93
+ - Refusing or cancelling login deletes nothing: saved results and purchase options remain usable.
94
+ - Seller authoring uses `itpay sell auth` separately.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 范围
6
6
 
7
- `itpay device` 只显示该命令组的帮助并退出,不访问 Backend、不读取或修改身份。当前唯一子命令是 `recover`。
7
+ `itpay device` 只显示该命令组的帮助并退出,不访问 Backend、不读取或修改身份。当前子命令是 `recover`(运营确认 Backend 重置后删除本地登记)和 `reset-key`(服务端拒绝当前私钥登记时放弃本地密钥)。
8
8
 
9
9
  仅在运营明确确认当前 Backend 的 Device 登记数据库已重建或清空后,删除本地该 Backend 的 v2 registration:
10
10
 
@@ -24,6 +24,18 @@ Type 或执行 `device recover`;应保留身份并重试原命令一次,持
24
24
 
25
25
  缺少确认参数返回 `backend_reset_confirmation_required`。普通 session 失效由 CLI 自动续期;revoked、quota、权限或未知 Backend 故障不得使用本命令。所有 Agent Type 使用相同输入和输出合同。
26
26
 
27
+ ## `itpay device reset-key`
28
+
29
+ 仅在服务端拒绝以当前私钥完成设备登记时使用(登记阶段持续返回 `internal_error`,或返回 `agent_device_key_rotated` / `agent_device_key_conflict`)。这是本地操作,不访问 Backend:
30
+
31
+ ```bash
32
+ itpay device reset-key --confirm-key-reset --json
33
+ ```
34
+
35
+ 命令删除本地 Ed25519 私钥并清空所有 Backend 的本地登记记录;下一次需要设备身份的命令会以全新密钥重新登记为新设备。服务端旧设备记录保留为孤儿,不会被删除或复用;原设备的额度谱系不迁移。先尝试普通重试——Backend 会把已验证私钥的重复登记幂等挂回原设备,只有在该修复不可用或私钥已被服务端轮换/冲突时才需要本命令。
36
+
37
+ 缺少确认参数返回 `key_reset_confirmation_required`;本地文件操作失败返回 `device_key_reset_failed`。本命令不得删除 Cart、operation journal 或 `~/.itpay-v3` 的其他内容。
38
+
27
39
  ## 参数
28
40
 
29
41
  | 参数 | 必填 | 说明 |
@@ -16,9 +16,15 @@ itpay services action <service_execution_id> --action <action_type>
16
16
  [--actor-type <actor_type>] [--actor-id <actor_id>]
17
17
  [--status <pending|approved|rejected|expired|cancelled>]
18
18
  [--candidate <rank> | --result-item <result_item_id>]
19
- [--required-before <step>] [--input <key=value> ...] [--json]
19
+ [--required-before <step>] [--input <key=value> ...]
20
+ [--input-json <file>] [--json]
20
21
  ```
21
22
 
23
+ | 选项 | 说明 |
24
+ |---|---|
25
+ | `--input <key=value>` | 逐项提交 action input;值可为 JSON 字面量(对象/数组/布尔/数字),可重复。 |
26
+ | `--input-json <file>` | UTF-8 JSON 文件;顶层必须是 object,整体作为 action input;文件不超过 256 KiB。适合嵌套结构(如订票确认的 `seat_preferences`)。**与 `--input` 互斥**:同时给出时在发起请求前返回 `service_action_invalid`。文件不存在、非合法 JSON 或顶层非 object 同样在本地报结构化错误,不发生网络写。 |
27
+
22
28
  普通 Agent 优先使用 `--candidate <rank>`。CLI 只从当前 Execution 的 `current_result_items` 解析 Result Item ID;Backend 再读取权威 Invocation 和 Stable Hash。Agent 不提交 Hash,也不能使用其他 Execution 或外部来源的候选。`--result-item` 只用于已持有当前 Execution 内部句柄的受控恢复,不应要求用户提供。
23
29
 
24
30
  ## 标准输出
@@ -45,6 +51,8 @@ itpay services action <service_execution_id> --action <action_type>
45
51
 
46
52
  rank 不存在、属于旧结果集或其他 Execution、action 不允许、status 非法时均不写 action;返回结构化错误并且只引导同一 Execution 的 `services next`。不得新建 Execution、重新 invoke 或构造候选 ID。相同候选重试幂等;同一结果集改选另一个候选返回冲突,不覆盖已批准事实。
47
53
 
54
+ 订票类 `workflow:confirm_booking` 动作要求嵌套输入(`passengers`、`seat_type`、逐乘客 `seat_preferences`、`draft_revision`、`notice_version` 等),一律经 `--input-json <file>` 提交完整 JSON 对象。`draft_revision` 必须等于 `services next` 投影的 `context.review.draft_revision`;服务端拒绝 `booking_review_changed` 时重新读取后重试,不得自行递增。乘客姓名、证件号等身份信息不在此提交,仍由受保护 Checkout 页收集。
55
+
48
56
  ## Agent Type / Host
49
57
 
50
58
  所有正式支持的 Local Agent Type 行为相同。需要人确认时 instruction 必须明确“先询问用户”,不能因 Desktop Host 自动代替用户选择。
@@ -6,7 +6,11 @@
6
6
 
7
7
  为单个 Service Execution 快速创建或恢复 Checkout,并按 Host 向人交接付款入口。它是 `services quote -> cart add --quote -> buy --cart` 的单项快捷方式,必须复用相同的 Quote、Cart 和 Checkout Use Case。
8
8
 
9
- **上游:** `services next` 返回的 `prepare_quote` capability 和已验证输入。
9
+ 未注册/未登录用户不要求先执行 `itpay auth login`(Path B):官方 checkout 页内完成必要的钱包认证、创建或复用账号并取得页面权限;人在页面上确认旅客、通知联系方式和最终报价后付款。支付确认沿用既有设备绑定机制,不再独立绑定。checkout 内完成账号认证不等于账号已通过手机号验证;钱包显示名、账号手机号与乘车人身份互相独立。
10
+
11
+ **通知联系方式(铁路单):** 订单级联系策略要求至少一个可用通道——邮箱,或在 checkout 页内经验证的大陆手机号(order-contact 验证码只校验收件人,不创建/合并登录身份)。有效邮箱即可独立完成 checkout;SMS 不是必填项,SMS 能力未配置时不得阻塞邮箱通道。该策略仅适用于铁路订单,企知道等既有服务的联系策略不变。乘客证件手机号、账号登录手机号与通知收件人是三个独立事实,互不替代。
12
+
13
+ **上游:** `services next` 返回的 `prepare_quote` capability 和已验证输入。铁路购票执行必须先通过 `workflow:confirm_booking` review(见 `services next` 的 `booking_review_required`),后端才生成可支付报价;review 开放期间不创建 Checkout。
10
14
  **下游:** 人完成 Checkout,随后 `checkout` 或 `services next`。
11
15
 
12
16
  ## 语法与参数
@@ -57,6 +57,8 @@ itpay services get <service_execution_id> [--json]
57
57
 
58
58
  `timeline` 只保留最近 20 个公开节点;超过时增加 `timeline_truncated: true`。它不返回 event ID 或 `redacted_summary`。交付存在时可增加 `delivery_mode`;退款锁存在时增加 `access_locked` 和退款 ID/status,并优先返回锁定 instruction。
59
59
 
60
+ 订票类 Execution 的 `workflow:confirm_booking` 待确认/待修订状态不在 timeline 中展开细节——行程、`seat_options` 与当前 `draft_revision` 一律通过 `services next` 的 `booking_review_required` 投影读取;`get` 只用于确认执行仍处于可继续状态。
61
+
60
62
  ## 异常处理
61
63
 
62
64
  默认不返回原始 events、Provider metadata、内部 bindings、Arazzo workflow、capability schema、candidate hash、client context 或完整 payload。需要完整但仍经服务端脱敏的事件时才运行 `services events`。
@@ -197,6 +197,87 @@ itpay services next <service_execution_id> [--json]
197
197
 
198
198
  `succeeded` 退款改为“交付永久关闭”,并返回 `next: null`。取消、拒绝或确定未产生资金影响的失败退款不再阻塞,但旧 grant 不会复活;用户必须重新授权。
199
199
 
200
+ ## 额度暂停(试用/限流)
201
+
202
+ 查询 Execution 因免费试用耗尽或限流被暂停时,状态为 `login_required` 或 `rate_limited`(不是 `failed`)。Execution 保留输入,可用同一 `services run <service> --execution <id> --json` 恢复——登录后或限流窗口过后从额度节点继续,不重放已完成阶段,不要新建 Execution 或重述行程。
203
+
204
+ ```json
205
+ {
206
+ "status": "login_required",
207
+ "result": {
208
+ "service_execution_id": "<id>",
209
+ "service_id": "itpay-rail-exact",
210
+ "admission": "login_required",
211
+ "query_quota": [
212
+ { "service_id": "itpay-rail-exact", "used": 2, "limit": 2, "remaining": 0 },
213
+ { "service_id": "itpay-rail-smart", "used": 0, "limit": 2, "remaining": 2 }
214
+ ]
215
+ },
216
+ "instruction": "如实告知该服务的免费试用次数与剩余;请用户登录后继续,原查询无需重新输入。",
217
+ "next": { "command": "itpay auth login --json", "reason": "官方登录绑定后继续免费查询" },
218
+ "recovery": [
219
+ { "command": "itpay services run itpay-rail-exact --execution <id> --json", "when": "auth_bound" }
220
+ ]
221
+ }
222
+ ```
223
+
224
+ `rate_limited` 只等待 `retry_after_ms` 后恢复同一执行;不需要登录、不扣试用。
225
+
226
+ ## 查询结果中的购买承接
227
+
228
+ 铁路查询结果项可携带服务端签发的购票承接 `booking_offer`:
229
+
230
+ ```json
231
+ {
232
+ "rank": 1,
233
+ "title": "C6908 二等座 11:48→12:20",
234
+ "safe_payload": { "...": "..." },
235
+ "booking_offer": {
236
+ "service_id": "itpay-rail-booking",
237
+ "selection_token": "sel_...",
238
+ "expires_at": "2026-09-20T04:00:00Z"
239
+ }
240
+ }
241
+ ```
242
+
243
+ 用户明确选择某班次/席别并要求购买时,用该 token 开启购票执行:
244
+
245
+ ```bash
246
+ itpay services run itpay-rail-booking --input-json booking.json --json
247
+ # booking.json: {"passengers": 1, "selection": {"token": "<selection_token>", "seat_type": "O"}}
248
+ ```
249
+
250
+ `booking_offer` 只是受条件约束的可选承接:查询完成本身已满足查询目标,不自动购买;过期 token 返回明确错误,恢复方式是按 `services run` 输入合同重新查询或显式提供 `legs`。
251
+
252
+ ## 订票确认(booking review)
253
+
254
+ `itpay-rail-booking` 在报价前停在 `workflow:confirm_booking` 人工确认节点。`services next` 返回 `booking_review_required`,`human_action.context.review` 携带服务端投影:`draft_revision`、`legs`(含可编码的 `seat_options`)、`notice_version` 等。乘客身份信息永不在此投影,也不经 CLI 提交。
255
+
256
+ ```json
257
+ {
258
+ "status": "booking_review_required",
259
+ "result": {
260
+ "service_execution_id": "<id>",
261
+ "service_id": "itpay-rail-booking",
262
+ "human_action": {
263
+ "action_type": "workflow:confirm_booking",
264
+ "context": { "review": { "draft_revision": 1, "legs": [], "seat_options": {} } }
265
+ },
266
+ "required_fields": ["draft_revision", "passengers", "seat_type", "seat_preferences", "party_preference", "fallback", "notice_version", "accept_non_guaranteed"]
267
+ },
268
+ "instruction": "向用户展示行程与可选席别/座位偏好词表,并明确告知:座位偏好仅为请求、不保证分配。收集后经 --input-json 提交完整 JSON;draft_revision 以服务端当前值为准。",
269
+ "next": { "command": "itpay services action <id> --action workflow:confirm_booking --actor-type human --status approved --input-json <file> --json", "reason": "提交用户确认后的行程确认/修订" },
270
+ "recovery": [{ "command": "itpay services next <id> --json", "reason": "重新读取当前 draft_revision 后重试" }]
271
+ }
272
+ ```
273
+
274
+ 规则:
275
+
276
+ - `seat_preferences` 逐乘客一条,`passenger_index` 从 0 起、人数 1–5、取值限该席别 `seat_options`;`party_preference`、`fallback=automatic_assignment`、`notice_version`、`accept_non_guaranteed=true` 全部必填。
277
+ - `draft_revision` 必须等于投影值;服务端返回 `booking_review_changed` 时重新 `services next` 读取后重试,不得自行递增或重放旧 revision。
278
+ - 付款暂停且 review 仍开放时,`next` 指向 Checkout 恢复,`recovery` 附带修订命令(`services action … confirm_booking --input-json`)——修订回到同一执行重新报价,不产生第二笔订单或付款。
279
+ - 支付已在途/完成后 review 关闭,修订被拒绝;按返回的同一订单状态继续,不得新建执行。
280
+
200
281
  ## 异常处理
201
282
 
202
283
  execution 不存在或不属于当前设备/账号时返回错误信封,并仅建议: