@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.
- package/README.md +8 -4
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/catalog.js +3 -2
- package/dist/src/commands/checkout.js +23 -13
- package/dist/src/commands/guidance.js +37 -13
- package/dist/src/commands/readyz.js +3 -3
- package/dist/src/commands/services.js +122 -41
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +116 -7
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/config.js +5 -13
- package/dist/src/state/device_authority.js +174 -56
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +19 -5
- package/docs/agent/buyer/payment-flow.json +5 -1
- package/docs/agent/buyer/quickstart.json +12 -4
- package/docs/cli-reference/agent-types.md +9 -3
- package/docs/cli-reference/commands/catalog/list.md +1 -1
- package/docs/cli-reference/commands/checkout.md +4 -4
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/install.md +3 -1
- package/docs/cli-reference/commands/readyz.md +4 -5
- package/docs/cli-reference/commands/services/action.md +10 -5
- package/docs/cli-reference/commands/services/checkout.md +3 -3
- package/docs/cli-reference/commands/services/invoke.md +6 -6
- package/docs/cli-reference/commands/services/next.md +23 -4
- package/docs/cli-reference/commands/services/quote.md +5 -1
- package/docs/cli-reference/commands/services/start.md +5 -3
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +4 -1
- package/docs/cli-reference/index.md +1 -0
- package/package.json +1 -1
- 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,
|
|
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
|
|
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 (!
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
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
|
-
|
|
63
|
-
|
|
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(
|
|
67
|
-
const registered = await this.signedJSON("/v1/agent-instances", { agent_type: agentType },
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
return { state, agentType, session, privateKey };
|
|
118
|
+
return this.ensureSession(registration, agentType, privateKey, forceSession);
|
|
73
119
|
}
|
|
74
|
-
async enroll(agentType) {
|
|
75
|
-
const
|
|
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),
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
-
|
|
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": "
|
|
29
|
-
"next": { "command": "itpay checkout --id <checkout_id> --token <display_token>", "reason": "
|
|
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
|
|
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
|
-
**下游:**
|
|
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
|
|
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
|
-
`
|
|
39
|
-
|
|
38
|
+
本命令不渲染 Host 内容。若已声明 Agent Type,`result.agent_type` 会确认该类型,且返回的 Skill 命令保留同一 `--agent-type`;未声明时 Skill 会先引导 `install`。
|