@itpay/cli 2.0.5 → 2.0.7

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 (34) hide show
  1. package/README.md +8 -4
  2. package/dist/src/client/http.js +29 -23
  3. package/dist/src/commands/catalog.js +3 -2
  4. package/dist/src/commands/checkout.js +23 -13
  5. package/dist/src/commands/guidance.js +37 -13
  6. package/dist/src/commands/readyz.js +3 -3
  7. package/dist/src/commands/services.js +122 -41
  8. package/dist/src/commands/skill.js +55 -0
  9. package/dist/src/main.js +116 -7
  10. package/dist/src/state/agent_type.js +19 -0
  11. package/dist/src/state/config.js +5 -13
  12. package/dist/src/state/device_authority.js +174 -56
  13. package/docs/agent/buyer/catalog-list.json +2 -1
  14. package/docs/agent/buyer/identity-and-sessions.json +64 -0
  15. package/docs/agent/buyer/install-and-setup.json +19 -5
  16. package/docs/agent/buyer/payment-flow.json +5 -1
  17. package/docs/agent/buyer/quickstart.json +12 -4
  18. package/docs/cli-reference/agent-types.md +9 -3
  19. package/docs/cli-reference/commands/catalog/list.md +1 -1
  20. package/docs/cli-reference/commands/checkout.md +4 -4
  21. package/docs/cli-reference/commands/device.md +13 -0
  22. package/docs/cli-reference/commands/install.md +3 -1
  23. package/docs/cli-reference/commands/readyz.md +4 -5
  24. package/docs/cli-reference/commands/services/action.md +10 -5
  25. package/docs/cli-reference/commands/services/checkout.md +3 -3
  26. package/docs/cli-reference/commands/services/invoke.md +6 -6
  27. package/docs/cli-reference/commands/services/next.md +23 -4
  28. package/docs/cli-reference/commands/services/quote.md +5 -1
  29. package/docs/cli-reference/commands/services/start.md +5 -3
  30. package/docs/cli-reference/commands/skill.md +17 -0
  31. package/docs/cli-reference/conventions.md +4 -1
  32. package/docs/cli-reference/index.md +1 -0
  33. package/package.json +1 -1
  34. package/skills/itpay-buyer/SKILL.md +33 -6
@@ -1,10 +1,11 @@
1
- import { createHash, createPrivateKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
5
  const PROTECTED_PATHS = ["/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds"];
6
6
  export class DeviceAuthority {
7
7
  baseURL;
8
+ backendKey;
8
9
  requestedAgentType;
9
10
  compatibilityHeaders;
10
11
  statePath;
@@ -13,6 +14,7 @@ export class DeviceAuthority {
13
14
  pending;
14
15
  constructor(options) {
15
16
  this.baseURL = options.baseURL.replace(/\/$/, "");
17
+ this.backendKey = normalizeBackendKey(options.baseURL);
16
18
  this.requestedAgentType = options.requestedAgentType;
17
19
  this.compatibilityHeaders = options.compatibilityHeaders;
18
20
  const root = resolve(homedir(), ".itpay-v3", "device");
@@ -47,54 +49,93 @@ export class DeviceAuthority {
47
49
  }
48
50
  return this.pending;
49
51
  }
52
+ async recoverAuthorization() {
53
+ await withFileLock(`${this.statePath}.lock`, async () => {
54
+ const state = this.readState();
55
+ if (!state || !this.requestedAgentType)
56
+ return;
57
+ const registration = state.registrations[this.backendKey];
58
+ if (!registration)
59
+ return;
60
+ delete registration.sessions[this.requestedAgentType];
61
+ this.writeState(state);
62
+ });
63
+ }
64
+ async recoverBackendReset() {
65
+ return withFileLock(`${this.statePath}.lock`, async () => {
66
+ const state = this.readState();
67
+ const registration = state?.registrations[this.backendKey];
68
+ if (!state || !registration)
69
+ return { removed: false, agentTypes: [] };
70
+ const agentTypes = Object.keys(registration.agentInstances).sort();
71
+ delete state.registrations[this.backendKey];
72
+ this.writeState(state);
73
+ return { removed: true, agentTypes };
74
+ });
75
+ }
50
76
  async prepareAuthorization() {
51
- let state = this.readState();
52
- const agentType = this.requestedAgentType ?? firstAgentType(state);
77
+ let state = this.readState() ?? emptyDeviceState();
78
+ const agentType = this.requestedAgentType;
53
79
  if (!agentType) {
54
80
  throw new Error("agent type is required for ItPay commerce; pass --agent-type <type> or set ITPAY_AGENT_TYPE");
55
81
  }
56
82
  let privateKey = this.readPrivateKey();
57
- if (!state || !privateKey) {
58
- const enrolled = await this.enroll(agentType);
59
- state = enrolled.state;
60
- privateKey = enrolled.privateKey;
83
+ if (!privateKey) {
84
+ const pair = generateKeyPairSync("ed25519");
85
+ privateKey = pair.privateKey;
86
+ this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
87
+ state = emptyDeviceState();
61
88
  }
62
- if (!state.agentInstances[agentType]) {
63
- const existingType = firstAgentType(state);
89
+ let registration = state.registrations[this.backendKey];
90
+ if (!registration && state.legacyRegistration) {
91
+ try {
92
+ await this.ensureRegistrationAgentType(state.legacyRegistration, agentType, privateKey, true);
93
+ registration = state.legacyRegistration;
94
+ delete state.legacyRegistration;
95
+ }
96
+ catch (error) {
97
+ if (!canMovePastLegacyRegistration(error))
98
+ throw error;
99
+ }
100
+ }
101
+ if (!registration) {
102
+ registration = await this.enroll(agentType, privateKey);
103
+ }
104
+ state.registrations[this.backendKey] = registration;
105
+ const session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
106
+ this.writeState(state);
107
+ return { state: registration, agentType, session, privateKey };
108
+ }
109
+ async ensureRegistrationAgentType(registration, agentType, privateKey, forceSession) {
110
+ if (!registration.agentInstances[agentType]) {
111
+ const existingType = firstAgentType(registration);
64
112
  if (!existingType)
65
113
  throw new Error("device has no registered agent instance");
66
- const existingSession = await this.ensureSession(state, existingType, privateKey);
67
- const registered = await this.signedJSON("/v1/agent-instances", { agent_type: agentType }, state, existingType, existingSession, privateKey);
68
- state.agentInstances[agentType] = registered.agent_instance_id;
69
- this.writeState(state);
114
+ const existingSession = await this.ensureSession(registration, existingType, privateKey, forceSession);
115
+ const registered = await this.signedJSON("/v1/agent-instances", { agent_type: agentType }, registration, existingType, existingSession, privateKey);
116
+ registration.agentInstances[agentType] = registered.agent_instance_id;
70
117
  }
71
- const session = await this.ensureSession(state, agentType, privateKey);
72
- return { state, agentType, session, privateKey };
118
+ return this.ensureSession(registration, agentType, privateKey, forceSession);
73
119
  }
74
- async enroll(agentType) {
75
- const pair = generateKeyPairSync("ed25519");
76
- const publicJWK = pair.publicKey.export({ format: "jwk" });
120
+ async enroll(agentType, privateKey) {
121
+ const publicJWK = createPublicKey(privateKey).export({ format: "jwk" });
77
122
  if (!publicJWK.x)
78
123
  throw new Error("unable to export Ed25519 public key");
79
124
  const publicKey = Buffer.from(publicJWK.x, "base64url").toString("base64");
80
125
  const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
81
126
  const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
82
- 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), pair.privateKey).toString("base64") });
83
- const state = {
84
- schemaVersion: "itpay.device.v1",
127
+ 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") });
128
+ return {
85
129
  deviceID: verified.agent_device_id,
86
130
  deviceKeyID: verified.agent_device_key_id,
87
131
  quotaLineageID: verified.quota_lineage_id,
88
132
  agentInstances: { [verified.agent_type]: verified.agent_instance_id },
89
133
  sessions: {},
90
134
  };
91
- this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
92
- this.writeState(state);
93
- return { state, privateKey: pair.privateKey };
94
135
  }
95
- async ensureSession(state, agentType, privateKey) {
136
+ async ensureSession(state, agentType, privateKey, force = false) {
96
137
  const existing = state.sessions[agentType];
97
- if (existing && Date.parse(existing.expiresAt) > Date.now() + 60_000)
138
+ if (!force && existing && Date.parse(existing.expiresAt) > Date.now() + 60_000)
98
139
  return existing;
99
140
  const instanceID = state.agentInstances[agentType];
100
141
  if (!instanceID)
@@ -107,7 +148,6 @@ export class DeviceAuthority {
107
148
  const verified = await this.publicJSON(`/v1/agent-device-session-challenges/${encodeURIComponent(challenge.agent_device_session_challenge_id)}/verify`, { challenge: challenge.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
108
149
  const session = { token: verified.session_token, expiresAt: verified.expires_at };
109
150
  state.sessions[agentType] = session;
110
- this.writeState(state);
111
151
  return session;
112
152
  }
113
153
  async signedJSON(path, bodyValue, state, agentType, session, privateKey) {
@@ -137,16 +177,26 @@ export class DeviceAuthority {
137
177
  });
138
178
  const payload = await response.json().catch(() => ({}));
139
179
  if (!response.ok)
140
- throw new Error(payload.message || payload.code || `ItPay device request failed: ${response.status}`);
180
+ throw new DeviceAuthorizationError(response.status, payload.code, payload.message || payload.code || `ItPay device request failed: ${response.status}`);
141
181
  return payload;
142
182
  }
143
183
  readState() {
144
184
  if (!existsSync(this.statePath))
145
185
  return undefined;
146
186
  try {
147
- return JSON.parse(readFileSync(this.statePath, "utf8"));
187
+ const parsed = JSON.parse(readFileSync(this.statePath, "utf8"));
188
+ if (parsed.schemaVersion === "itpay.device.v2")
189
+ return parsed;
190
+ if (parsed.schemaVersion === "itpay.device.v1") {
191
+ const { schemaVersion: _, ...legacyRegistration } = parsed;
192
+ return { ...emptyDeviceState(), legacyRegistration };
193
+ }
194
+ return undefined;
148
195
  }
149
- catch {
196
+ catch (error) {
197
+ const stateError = asDeviceStateError(error, "read_state");
198
+ if (stateError)
199
+ throw stateError;
150
200
  return undefined;
151
201
  }
152
202
  }
@@ -156,62 +206,130 @@ export class DeviceAuthority {
156
206
  try {
157
207
  return createPrivateKey(readFileSync(this.privateKeyPath, "utf8"));
158
208
  }
159
- catch {
209
+ catch (error) {
210
+ const stateError = asDeviceStateError(error, "read_private_key");
211
+ if (stateError)
212
+ throw stateError;
160
213
  return undefined;
161
214
  }
162
215
  }
163
- writeState(state) { atomicOwnerOnlyWrite(this.statePath, JSON.stringify(state, null, 2)); }
164
- writePrivateKey(value) { atomicOwnerOnlyWrite(this.privateKeyPath, value); }
216
+ writeState(state) { atomicOwnerOnlyWrite(this.statePath, JSON.stringify(state, null, 2), "write_state"); }
217
+ writePrivateKey(value) { atomicOwnerOnlyWrite(this.privateKeyPath, value, "write_private_key"); }
218
+ }
219
+ export class DeviceAuthorizationError extends Error {
220
+ status;
221
+ code;
222
+ constructor(status, code, message) {
223
+ super(message);
224
+ this.status = status;
225
+ this.code = code;
226
+ this.name = "DeviceAuthorizationError";
227
+ }
228
+ }
229
+ export class DeviceStateError extends Error {
230
+ operation;
231
+ causeCode;
232
+ code = "device_state_unwritable";
233
+ constructor(operation, causeCode) {
234
+ super(`ItPay device state operation failed: ${operation} (${causeCode})`);
235
+ this.operation = operation;
236
+ this.causeCode = causeCode;
237
+ this.name = "DeviceStateError";
238
+ }
239
+ }
240
+ function emptyDeviceState() {
241
+ return { schemaVersion: "itpay.device.v2", registrations: {} };
242
+ }
243
+ function firstAgentType(state) { return Object.keys(state.agentInstances)[0]; }
244
+ function canMovePastLegacyRegistration(error) {
245
+ return error instanceof DeviceAuthorizationError && (error.code === "agent_device_revoked" || error.status === 404);
246
+ }
247
+ function normalizeBackendKey(value) {
248
+ const url = new URL(value);
249
+ url.search = "";
250
+ url.hash = "";
251
+ url.pathname = url.pathname.replace(/\/+$/, "");
252
+ return url.toString().replace(/\/$/, "");
165
253
  }
166
- function firstAgentType(state) { return state ? Object.keys(state.agentInstances)[0] : undefined; }
167
254
  function sha256(value) { return `sha256:${createHash("sha256").update(value).digest("hex")}`; }
168
255
  function enrollmentProofMessage(id, challenge) { return `itpay-device-enrollment/v1\n${id}\n${challenge}`; }
169
256
  function deviceSessionProofMessage(id, challenge) { return `itpay-device-session/v1\n${id}\n${challenge}`; }
170
257
  function requestProofMessage(method, path, bodyHash, timestamp, jti) { return ["itpay-agent-request/v1", method, path, bodyHash, timestamp, jti].join("\n"); }
171
- function atomicOwnerOnlyWrite(path, value) {
172
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
258
+ function atomicOwnerOnlyWrite(path, value, operation) {
173
259
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
174
- writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
175
- chmodSync(temporary, 0o600);
176
- renameSync(temporary, path);
177
- chmodSync(path, 0o600);
260
+ try {
261
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
262
+ writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
263
+ chmodSync(temporary, 0o600);
264
+ renameSync(temporary, path);
265
+ chmodSync(path, 0o600);
266
+ }
267
+ catch (error) {
268
+ try {
269
+ unlinkSync(temporary);
270
+ }
271
+ catch { /* best-effort cleanup */ }
272
+ throw asDeviceStatePathError(error, operation) ?? error;
273
+ }
178
274
  }
179
275
  async function withFileLock(path, run) {
180
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
181
- let descriptor;
276
+ try {
277
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
278
+ }
279
+ catch (error) {
280
+ throw asDeviceStatePathError(error, "prepare_lock") ?? error;
281
+ }
282
+ let acquired = false;
182
283
  for (let attempt = 0; attempt < 200; attempt += 1) {
183
284
  try {
184
- descriptor = openSync(path, "wx", 0o600);
285
+ mkdirSync(path, { mode: 0o700 });
286
+ acquired = true;
185
287
  break;
186
288
  }
187
289
  catch (error) {
188
290
  const code = error.code;
189
291
  if (code !== "EEXIST")
190
- throw error;
292
+ throw asDeviceStateError(error, "acquire_lock") ?? error;
191
293
  try {
192
294
  if (Date.now() - statSync(path).mtimeMs > 30_000)
193
- unlinkSync(path);
295
+ removeLock(path, "remove_stale_lock");
194
296
  }
195
297
  catch (statError) {
196
- if (statError.code !== "ENOENT")
197
- throw statError;
298
+ if (statError.code !== "ENOENT") {
299
+ throw asDeviceStateError(statError, "inspect_lock") ?? statError;
300
+ }
198
301
  }
199
302
  await new Promise((resolve) => setTimeout(resolve, 25));
200
303
  }
201
304
  }
202
- if (descriptor === undefined)
305
+ if (!acquired)
203
306
  throw new Error("timed out waiting for ItPay device identity lock");
204
307
  try {
205
308
  return await run();
206
309
  }
207
310
  finally {
208
- closeSync(descriptor);
209
- try {
311
+ removeLock(path, "release_lock");
312
+ }
313
+ }
314
+ function removeLock(path, operation) {
315
+ try {
316
+ if (statSync(path).isDirectory())
317
+ rmdirSync(path);
318
+ else
210
319
  unlinkSync(path);
211
- }
212
- catch (error) {
213
- if (error.code !== "ENOENT")
214
- throw error;
215
- }
216
320
  }
321
+ catch (error) {
322
+ if (error.code !== "ENOENT")
323
+ throw asDeviceStateError(error, operation) ?? error;
324
+ }
325
+ }
326
+ function asDeviceStateError(error, operation) {
327
+ const code = error.code;
328
+ return code === "EACCES" || code === "EPERM" || code === "EROFS" || code === "ENOTDIR" || code === "EISDIR"
329
+ ? new DeviceStateError(operation, code)
330
+ : undefined;
331
+ }
332
+ function asDeviceStatePathError(error, operation) {
333
+ const code = error.code;
334
+ return code === "EEXIST" ? new DeviceStateError(operation, code) : asDeviceStateError(error, operation);
217
335
  }
@@ -27,7 +27,8 @@
27
27
  "Read the variant IDs and prices from the output; do not invent them.",
28
28
  "Explain the customer journey from service_flow instead of presenting every variant as an unrelated service.",
29
29
  "State free quota, paid continuation price, email requirement, and claim purpose exactly as published; do not exaggerate or omit conditions.",
30
- "If the buyer wants to search by keyword, filter the catalog output client-side (no backend search yet)."
30
+ "If the buyer wants to search by keyword, filter the catalog output client-side (no backend search yet).",
31
+ "After services start, submit service keywords only through the returned capability command's --input key=value options; --target is never a search keyword."
31
32
  ],
32
33
  "next_docs": [
33
34
  {
@@ -0,0 +1,64 @@
1
+ {
2
+ "schema_version": "itp.agent_doc.v1",
3
+ "role": "buyer",
4
+ "topic": "identity-and-sessions",
5
+ "title": "Device Identity And Session Recovery",
6
+ "purpose": "Explain stable local identity, Backend-scoped registrations, Agent Type instances, and bounded automatic session recovery.",
7
+ "when_to_use": [
8
+ "A different Agent window or runtime starts using an existing CLI installation.",
9
+ "The CLI reports device_state_unwritable, or the Backend reports agent_identity_required, agent_device_session_required, or agent_device_revoked.",
10
+ "The configured ITPAY_BACKEND_URL changes between dev, test, and app."
11
+ ],
12
+ "identity_model": {
13
+ "local_key": "One owner-only Ed25519 private key for the local installation.",
14
+ "backend_registration": "One independent server registration per normalized Backend API base URL.",
15
+ "agent_instance": "One instance per agent_type under each Backend registration; same-type windows and chats reuse it.",
16
+ "presentation": "Host and target route output only and never identify the Agent or provide business input."
17
+ },
18
+ "automatic_recovery": [
19
+ "The CLI reuses a valid cached device session.",
20
+ "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.",
21
+ "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.",
22
+ "A revoked v2 registration requires explicit operator recovery; it is never silently replaced."
23
+ ],
24
+ "local_state_recovery": [
25
+ "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.",
26
+ "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.",
27
+ "Do not manually create a lock, switch Node runtimes, delete identity, or rotate Agent Type as a retry strategy."
28
+ ],
29
+ "commands": [
30
+ {
31
+ "intent": "reload the complete rules",
32
+ "command": "itpay --agent-type <agent_type> skill show itpay-buyer --json",
33
+ "success_signal": "the full Skill is returned and next keeps the same Agent Type"
34
+ },
35
+ {
36
+ "intent": "confirm the selected Backend is available",
37
+ "command": "itpay --agent-type <agent_type> readyz --json",
38
+ "success_signal": "Backend is available and onboarding returns to the Skill"
39
+ },
40
+ {
41
+ "intent": "recover after an operator-confirmed reset of the selected Backend registration database",
42
+ "command": "itpay --agent-type <agent_type> device recover --confirm-backend-reset --json",
43
+ "success_signal": "only the selected Backend registration is removed; the private key and other Backend registrations remain"
44
+ }
45
+ ],
46
+ "agent_rules": [
47
+ "Use the real runtime type and keep it stable across every command in the flow.",
48
+ "Keep the same CLI/Node launcher and Host-approved permission context for the whole flow.",
49
+ "Treat a new window, chat, task, or process of the same runtime as the same Agent Type, not a new identity.",
50
+ "Expect dev, test, and app to have separate device IDs, quota lineage, Agent instances, and sessions even on the same machine.",
51
+ "After automatic recovery fails, stop and report the exact code, Backend URL, Agent Type, and command family without exposing private keys or tokens."
52
+ ],
53
+ "forbidden": [
54
+ "Do not delete ~/.itpay-v3, rotate the key, switch Agent Type, or repeatedly retry to obtain fresh quota.",
55
+ "Do not use device recover for session expiry, revocation, quota recovery, or an unconfirmed Backend failure.",
56
+ "Do not copy a server device ID from one Backend registration into another.",
57
+ "Do not use --target as identity or service input."
58
+ ],
59
+ "next_docs": [
60
+ { "condition": "Identity is healthy", "topic": "quickstart" },
61
+ { "condition": "Need setup details", "topic": "install-and-setup" }
62
+ ],
63
+ "search_terms": ["identity", "device", "session", "login", "authentication", "agent type", "backend", "revoked", "recover", "unwritable", "lock", "window", "chat"]
64
+ }
@@ -3,12 +3,22 @@
3
3
  "role": "buyer",
4
4
  "topic": "install-and-setup",
5
5
  "title": "Install And Identify The ItPay Agent Runtime",
6
- "purpose": "Install the CLI and select one of the five supported Agent Types without confusing Agent Type with Host.",
6
+ "purpose": "Install the CLI, load the complete Buyer Skill, and select one stable Agent Type without confusing identity, Backend, Host, target, or chat window.",
7
7
  "when_to_use": [
8
8
  "The CLI is being installed or upgraded.",
9
9
  "The agent needs to confirm its stable runtime identity and default output Host."
10
10
  ],
11
11
  "commands": [
12
+ {
13
+ "intent": "verify the API and enter packaged onboarding",
14
+ "command": "itpay readyz --json",
15
+ "success_signal": "status is ready and next points to skill show itpay-buyer"
16
+ },
17
+ {
18
+ "intent": "read the complete packaged operating contract",
19
+ "command": "itpay skill show itpay-buyer --json",
20
+ "success_signal": "status is shown, result.content contains the complete Skill, and next selects Agent Type or Catalog"
21
+ },
12
22
  {
13
23
  "intent": "list supported Agent Types",
14
24
  "command": "itpay install --json",
@@ -24,15 +34,19 @@
24
34
  "Install with npm install -g @itpay/cli.",
25
35
  "Use the default https://app.itpay.ai API unless an environment override is deliberate.",
26
36
  "Use one exact type: codex-desktop, codex-cli, claude-code-desktop, claude-code-cli, or workbuddy.",
27
- "Agent Type identifies the runtime and device instance. Host only controls human-facing rendering.",
28
- "Do not change Agent Type to reset quota or recover a failed command."
37
+ "One local private key is reused, but each exact Backend API base URL has its own server registration and quota lineage.",
38
+ "Within one Backend registration, each Agent Type has one Agent Instance; all windows and chats of the same type reuse it.",
39
+ "Agent Type identifies the runtime. Host controls rendering and target only identifies a presentation destination.",
40
+ "Keep the exact Agent Type in every next or recovery command; do not fall back to a type previously used by another runtime.",
41
+ "Do not change Agent Type or rotate local identity to reset quota or recover a failed command."
29
42
  ],
30
43
  "forbidden": [
31
44
  "Do not use codex, terminal, claude-code, or plain-chat as Agent Types.",
32
- "Do not claim that install writes host configuration or registers a device; it only prints instructions."
45
+ "Do not claim that install writes host configuration or registers a device; it only prints instructions.",
46
+ "Do not create a new identity for a different window, task, chat, or process of the same Agent Type."
33
47
  ],
34
48
  "next_docs": [
35
49
  { "condition": "Installation is verified", "topic": "quickstart" }
36
50
  ],
37
- "search_terms": ["install", "agent type", "host", "codex", "claude code", "workbuddy"]
51
+ "search_terms": ["install", "agent type", "host", "target", "device", "session", "identity", "backend", "codex", "claude code", "workbuddy"]
38
52
  }
@@ -28,13 +28,17 @@
28
28
  "agent_rules": [
29
29
  "Normal buyer flow opens the ItPay Checkout page; itpay pay and buy --pay are operator escape hatches.",
30
30
  "Use only handoff fields returned for the current Host and make them actually visible to the human.",
31
+ "Before creating a single-Service Checkout, send the exact price confirmation requested by the current instruction, stop, and wait for explicit human agreement.",
32
+ "After presenting a Checkout, stop. Run the returned next.command only after the human completes the payment action or asks to query the same Checkout.",
31
33
  "Payment is verified only by Backend Checkout or Order state, never by QR rendering, redirect, or user claim.",
32
34
  "A terminal payment state must never display another payment handoff."
33
35
  ],
34
36
  "forbidden": [
35
37
  "Do not render a provider QR in place of the ItPay Checkout handoff.",
36
38
  "Do not mix a display token from another Checkout.",
37
- "Do not create a replacement Checkout after an uncertain response; recover first."
39
+ "Do not create a replacement Checkout after an uncertain response; recover first.",
40
+ "Do not use services quote, cart, buy, or pay as a fallback when the single-Service checkout flow fails.",
41
+ "Do not start another Service Execution to bypass quota, candidate, quote, checkout, or delivery state."
38
42
  ],
39
43
  "next_docs": [
40
44
  { "condition": "Payment is verified", "topic": "orders-refunds" },
@@ -19,7 +19,12 @@
19
19
  {
20
20
  "intent": "verify compatibility",
21
21
  "command": "itpay --agent-type <agent_type> readyz --json",
22
- "success_signal": "status is ready and next points to catalog list"
22
+ "success_signal": "status is ready and next points to the complete packaged Buyer Skill"
23
+ },
24
+ {
25
+ "intent": "load the complete operating contract",
26
+ "command": "itpay --agent-type <agent_type> skill show itpay-buyer --json",
27
+ "success_signal": "status is shown and next points to catalog list with the same Agent Type"
23
28
  },
24
29
  {
25
30
  "intent": "discover published services",
@@ -45,7 +50,9 @@
45
50
  "agent_rules": [
46
51
  "Use the CLI as the control surface; do not call ItPay APIs directly or hardcode a service workflow.",
47
52
  "Treat result as current facts, instruction as how to use those facts, next as the preferred executable action, and recovery as exceptional paths.",
48
- "Run one state-changing command at a time and execute the exact next.command unchanged after filling only required user data.",
53
+ "Run one state-changing command at a time. Treat next.command as the preferred continuation: execute it unchanged after filling required user data only when the current result has not already satisfied the user's goal.",
54
+ "Keep the same explicit Agent Type through next and recovery commands; same-type windows reuse one Backend Agent Instance.",
55
+ "Use --target only for a Host presentation destination. Put capability business values in --input key=value exactly as required by the schema.",
49
56
  "Start a separate Service Execution for each independent service intent; quota remains shared according to Backend identity policy.",
50
57
  "Ask the user for required email or contact values and explain their purpose; never invent them.",
51
58
  "Agent-visible delivery is returned by services next. Protected delivery is read only by services read-result while a human grant is active.",
@@ -63,7 +70,8 @@
63
70
  { "condition": "Need payment recovery", "topic": "payment-flow" },
64
71
  { "condition": "Need Host-specific presentation", "topic": "render-hosts" },
65
72
  { "condition": "Need order or refund recovery", "topic": "orders-refunds" },
66
- { "condition": "Need install details", "topic": "install-and-setup" }
73
+ { "condition": "Need install or identity details", "topic": "install-and-setup" },
74
+ { "condition": "Need device session recovery rules", "topic": "identity-and-sessions" }
67
75
  ],
68
- "search_terms": ["quickstart", "service", "next", "recovery", "delivery", "refund"]
76
+ "search_terms": ["quickstart", "service", "next", "recovery", "delivery", "refund", "target", "input"]
69
77
  }
@@ -1,6 +1,8 @@
1
1
  # Agent Type And Host Contract
2
2
 
3
- `--agent-type` 表示谁在运行 CLI,用于设备登记、Agent 实例归属和定制 instruction。`--host` 表示输出展示在哪里,用于选择二维码、链接或消息的渲染方式。两者不可混用。
3
+ `--agent-type` 表示哪类运行时在运行 CLI,用于 Agent 实例归属和定制 instruction。`--host` 表示输出展示在哪里;`--target` 只是在某些 Host 中指定 chat/channel/open ID。三者不可混用,窗口、任务和对话也不是身份。
4
+
5
+ 本地只保存一把 Ed25519 私钥。每个规范化 Backend API base URL 独立登记 Device,因此 dev/test/app 分别拥有自己的 device ID、quota lineage、Agent instances 和 sessions。同一 Backend 下每个 `agent_type` 只有一个 Agent Instance;同类型的不同窗口、任务或聊天复用它,不追踪窗口 ID。
4
6
 
5
7
  ## 首批支持类型
6
8
 
@@ -15,10 +17,14 @@
15
17
  ## 通用规则
16
18
 
17
19
  - commerce 命令必须传 `--agent-type` 或设置 `ITPAY_AGENT_TYPE`。
18
- - Agent Type 必须稳定;同一运行时不得临时换名。
20
+ - Agent Type 必须真实且稳定;同类型窗口复用同一实例,不得临时换名。
21
+ - `next.command` 和 `recovery.command` 必须保留当前显式 Agent Type,不读取或回退到机器上其他类型。
19
22
  - 显式 `--host` 覆盖默认 Host,但不改变已登记的 Agent Type。
23
+ - `--target` 只路由人类展示,不是身份,也不是 capability 业务输入。
20
24
  - Host 只影响 `instruction` 和 `handoff`,不得改变金额、订单、权限、quota 或交付状态。
21
- - 非展示命令在五种 Agent Type 下返回相同业务结果,只允许 instruction 措辞不同。
25
+ - 五种 Agent Type 使用同一命令输入和 JSON 外壳;不得为单个 Agent Type 新增、删除或改名协议字段。
26
+ - 非展示命令在五种 Agent Type 下返回相同业务结果,只允许 `instruction` 措辞不同。只有 Host 客观无法展示某种媒介时,`handoff` 才按既有可选字段做最小裁剪。
27
+ - session 失效时 CLI 只续期并重试原请求一次;再次失败立即返回。revoked v2 Device 不自动换身份。
22
28
 
23
29
  ## Checkout Handoff 最小合同
24
30
 
@@ -31,7 +31,7 @@ itpay catalog list [--json]
31
31
  ]
32
32
  },
33
33
  "instruction": "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
34
- "next": { "command": "itpay services start <service_id>", "reason": "启动用户选择的服务" },
34
+ "next": { "command": "itpay --agent-type <agent_type> services start <service_id> --json", "reason": "启动用户选择的服务" },
35
35
  "recovery": []
36
36
  }
37
37
  ```
@@ -25,8 +25,8 @@ itpay checkout [--id <checkout_id>] [--token <display_token>]
25
25
  "status": "human_checkout_required",
26
26
  "result": { "checkout_id": "<checkout_id>", "payment": "pending", "amount": "<amount> <currency>" },
27
27
  "handoff": { "url": "<checkout_url>", "qr_local_path": "<host_optional_path>", "markdown": "<desktop_optional_markdown>" },
28
- "instruction": "把当前 Host 的二维码和付款链接展示给用户,然后等待用户操作;不要创建新 Checkout。",
29
- "next": { "command": "itpay checkout --id <checkout_id> --token <display_token>", "reason": "稍后查询同一笔 Checkout 状态" },
28
+ "instruction": "Backend 尚未确认付款。把当前同一 Checkout 的付款入口重新展示给用户,然后停止等待。不要声称付款成功,不要创建新 Checkout、Execution 或 Payment Intent。稍后仍然只执行 next.command 查询这一笔 Checkout。",
29
+ "next": { "command": "itpay checkout --id <checkout_id> --token <display_token> --json", "reason": "稍后只查询同一 Checkout" },
30
30
  "recovery": []
31
31
  }
32
32
  ```
@@ -37,8 +37,8 @@ itpay checkout [--id <checkout_id>] [--token <display_token>]
37
37
  {
38
38
  "status": "completed",
39
39
  "result": { "checkout_id": "<checkout_id>", "payment": "verified", "order_id": "<optional_order_id>", "service_execution_id": "<optional_id>" },
40
- "instruction": "付款已确认,不要再次展示付款二维码。",
41
- "next": { "command": "itpay services next <service_execution_id> --json", "reason": "读取履约状态" },
40
+ "instruction": "Backend 已确认这笔付款。不要再次展示付款入口,不要调用 pay,不要创建新 Checkout 或 Execution。现在只执行 next.command,读取同一 Execution 的履约结果。",
41
+ "next": { "command": "itpay services next <service_execution_id> --json", "reason": "读取同一笔已付款 Service Execution" },
42
42
  "recovery": []
43
43
  }
44
44
  ```
@@ -0,0 +1,13 @@
1
+ # `itpay device recover`
2
+
3
+ ## 范围
4
+
5
+ 仅在运营明确确认当前 Backend 的 Device 登记数据库已重建或清空后,删除本地该 Backend 的 v2 registration:
6
+
7
+ ```bash
8
+ itpay --agent-type <agent_type> device recover --confirm-backend-reset --json
9
+ ```
10
+
11
+ 命令使用 `ITPAY_BACKEND_URL` 选择唯一作用域,保留本地 Ed25519 私钥、其他 Backend registrations、Cart 和业务资源。它不访问 Backend,不自动创建新身份;返回的只读 `services list` 是重新登记入口。
12
+
13
+ 缺少确认参数返回 `backend_reset_confirmation_required`。普通 session 失效由 CLI 自动续期;revoked、quota、权限或未知 Backend 故障不得使用本命令。所有 Agent Type 使用相同输入和输出合同。
@@ -6,7 +6,7 @@
6
6
 
7
7
  **上游:** 安装或更新 `@itpay/cli`。
8
8
 
9
- **下游:** 使用真实 Agent Type 执行 `readyz`,随后读取 Catalog。
9
+ **下游:** 使用真实 Agent Type 执行 `readyz`,随后读取完整 Skill 和 Catalog。
10
10
 
11
11
  ## 语法与参数
12
12
 
@@ -87,6 +87,8 @@ itpay install [target] [--json]
87
87
 
88
88
  显式 `--host` 可以在后续 commerce 命令覆盖默认 Host,但不会改变 Agent Type 或设备归属。
89
89
 
90
+ 同一类型的不同窗口、任务和聊天复用当前 Backend 下的同一个 Agent Instance。切换 Backend 会在同一本地私钥下建立独立服务端登记,不会把 dev/test/app 的 Device ID 混用。
91
+
90
92
  ## 异常处理
91
93
 
92
94
  未知 target 返回:
@@ -5,7 +5,7 @@
5
5
  检查当前配置的 ItPay API 是否可用。它只做环境诊断,不登记设备、不创建业务资源。
6
6
 
7
7
  **上游:** CLI 安装和 Backend URL 配置。
8
- **下游:** `catalog list` 或失败后的网络/配置修复。
8
+ **下游:** 完整 `itpay-buyer` Skill,随后选择 Agent Type 或读取 Catalog。
9
9
 
10
10
  ## 语法与参数
11
11
 
@@ -23,8 +23,8 @@ itpay readyz [--json]
23
23
  {
24
24
  "status": "ready",
25
25
  "result": { "backend": "available" },
26
- "instruction": "ItPay 可用,可以读取服务目录。",
27
- "next": { "command": "itpay catalog list", "reason": "发现可用服务" },
26
+ "instruction": "ItPay 可用;先完整读取内置 Buyer Skill,再开始服务流程。",
27
+ "next": { "command": "itpay skill show itpay-buyer --json", "reason": "加载完整操作与安全规则" },
28
28
  "recovery": []
29
29
  }
30
30
  ```
@@ -35,5 +35,4 @@ itpay readyz [--json]
35
35
 
36
36
  ## Agent Type / Host
37
37
 
38
- `codex-desktop`、`codex-cli`、`claude-code-desktop`、`claude-code-cli`、`workbuddy` 行为相同;本命令不渲染 Host 内容。
39
-
38
+ 本命令不渲染 Host 内容。若已声明 Agent Type,`result.agent_type` 会确认该类型,且返回的 Skill 命令保留同一 `--agent-type`;未声明时 Skill 会先引导 `install`。