@perkos/agent-sdk 0.7.1 → 0.8.0-rc.1

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 (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +69 -17
  3. package/dist/builders.d.ts +5 -1
  4. package/dist/builders.d.ts.map +1 -1
  5. package/dist/builders.js +155 -11
  6. package/dist/builders.js.map +1 -1
  7. package/dist/client.d.ts +8 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +168 -26
  10. package/dist/client.js.map +1 -1
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +2 -0
  13. package/dist/constants.js.map +1 -1
  14. package/dist/custody/backend.d.ts +4 -0
  15. package/dist/custody/backend.d.ts.map +1 -0
  16. package/dist/custody/backend.js +149 -0
  17. package/dist/custody/backend.js.map +1 -0
  18. package/dist/custody/cli.d.ts +3 -0
  19. package/dist/custody/cli.d.ts.map +1 -0
  20. package/dist/custody/cli.js +43 -0
  21. package/dist/custody/cli.js.map +1 -0
  22. package/dist/custody/engine.d.ts +54 -0
  23. package/dist/custody/engine.d.ts.map +1 -0
  24. package/dist/custody/engine.js +115 -0
  25. package/dist/custody/engine.js.map +1 -0
  26. package/dist/custody/ledger.d.ts +24 -0
  27. package/dist/custody/ledger.d.ts.map +1 -0
  28. package/dist/custody/ledger.js +122 -0
  29. package/dist/custody/ledger.js.map +1 -0
  30. package/dist/custody/permit.d.ts +42 -0
  31. package/dist/custody/permit.d.ts.map +1 -0
  32. package/dist/custody/permit.js +91 -0
  33. package/dist/custody/permit.js.map +1 -0
  34. package/dist/custody/socket.d.ts +11 -0
  35. package/dist/custody/socket.d.ts.map +1 -0
  36. package/dist/custody/socket.js +118 -0
  37. package/dist/custody/socket.js.map +1 -0
  38. package/dist/evaluation-commitments.d.ts +47 -0
  39. package/dist/evaluation-commitments.d.ts.map +1 -0
  40. package/dist/evaluation-commitments.js +96 -0
  41. package/dist/evaluation-commitments.js.map +1 -0
  42. package/dist/index.d.ts +6 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/mcp/cli.d.ts +3 -0
  47. package/dist/mcp/cli.d.ts.map +1 -0
  48. package/dist/mcp/cli.js +36 -0
  49. package/dist/mcp/cli.js.map +1 -0
  50. package/dist/mcp/evaluation.d.ts +33 -0
  51. package/dist/mcp/evaluation.d.ts.map +1 -0
  52. package/dist/mcp/evaluation.js +141 -0
  53. package/dist/mcp/evaluation.js.map +1 -0
  54. package/dist/mcp/server.d.ts +27 -0
  55. package/dist/mcp/server.d.ts.map +1 -0
  56. package/dist/mcp/server.js +207 -0
  57. package/dist/mcp/server.js.map +1 -0
  58. package/dist/policy.d.ts.map +1 -1
  59. package/dist/policy.js +7 -2
  60. package/dist/policy.js.map +1 -1
  61. package/dist/service-fees.d.ts +15 -0
  62. package/dist/service-fees.d.ts.map +1 -0
  63. package/dist/service-fees.js +109 -0
  64. package/dist/service-fees.js.map +1 -0
  65. package/dist/types.d.ts +66 -2
  66. package/dist/types.d.ts.map +1 -1
  67. package/docs/ARCHITECTURE.md +18 -2
  68. package/docs/EXISTING_AGENT.md +123 -0
  69. package/docs/HERMES_BUYER.md +91 -0
  70. package/docs/HERMES_CHECKPOINTS.md +88 -0
  71. package/docs/HERMES_CUSTODY.md +164 -0
  72. package/docs/HERMES_MCP.md +157 -0
  73. package/docs/HERMES_PROVIDER.md +98 -0
  74. package/docs/RELEASE_0.8.0_RC1.md +60 -0
  75. package/docs/SERVICE_FEES.md +108 -0
  76. package/docs/TESTNET_QUICKSTART.md +123 -0
  77. package/docs/VALIDATION_AND_RELEASE.md +82 -0
  78. package/docs/WALLET_SIGNER_SETUP.md +52 -0
  79. package/examples/service-fees.ts +36 -0
  80. package/examples/testnet-journal.ts +57 -0
  81. package/examples/testnet-lifecycle.ts +188 -294
  82. package/examples/testnet.env.example +12 -14
  83. package/package.json +7 -1
@@ -1,305 +1,199 @@
1
- import {
2
- HeadlessSigner,
3
- PerkOSClient,
4
- PerkOSError,
1
+ /**
2
+ * Role-separated QA example. Preview is offline; live actions require explicit consent.
3
+ * No evaluator key, fake deliverable, automatic retries or mainnet path.
4
+ */
5
+ import { mkdirSync, readFileSync, rmdirSync } from "node:fs";
6
+ import { isAbsolute } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { PerkOSClient, prepareEvaluationJob, prepareEvaluationSubmission, evaluationJobId,
9
+ type CriteriaCommitmentInput, type EvaluationEvidence, type PerkOSSigner, type ServiceFeeAcceptance,
5
10
  } from "@perkos/agent-sdk";
6
- import type {
7
- ContractCallPlan,
8
- PerkOSSigner,
9
- TransactionConfirmation,
10
- TransactionReceipt,
11
- } from "@perkos/agent-sdk";
12
- import { fetchChainTip } from "./testnet-api.js";
13
-
14
- const NETWORK = "testnet" as const;
15
- const DEFAULT_API_URL = "https://api.testnet.hiro.so";
16
- const DRY_RUN_CLIENT = "ST1THWXQ8368SDN2MJGE4BMDKMCHZ2GSVTSQDA7QF";
17
- const DRY_RUN_PROVIDER = "ST3AZN3BSQYJ5VWMNG92N88Z4G9498VYSHDZD9EK";
18
- const DRY_RUN_EVALUATOR = "ST1YXCNCJT2NJZR6G4NYNE6NZ0CPDKPWKVJDRPKTJ";
19
- const DEMO_JOB_ID = 1n;
20
- const DEMO_AMOUNT = 100n;
21
-
22
- function json(value: unknown): string {
23
- return JSON.stringify(
24
- value,
25
- (_key, current) =>
26
- typeof current === "bigint" ? current.toString() : current,
27
- 2
28
- );
11
+ import { QuickstartJournal, checkpointedTransaction, intentHash } from "./testnet-journal.js";
12
+
13
+ const DEPLOYER = "ST16EWRC01S1SFWGBP63MW47VY8P3AYFA8VGEBGE5";
14
+ const API = "https://api.testnet.hiro.so";
15
+ const contracts = {
16
+ stxCommerce: `${DEPLOYER}.agentic-commerce-v6` as const,
17
+ sbtcCommerce: `${DEPLOYER}.sbtc-commerce-v5` as const,
18
+ };
19
+ const steps = ["register", "create", "set-budget", "fund", "assign", "submit", "evaluate", "status", "finalize"] as const;
20
+ type Step = typeof steps[number];
21
+ interface RunConfig extends Omit<CriteriaCommitmentInput, "contract"> {
22
+ role: "client" | "provider";
23
+ provider: string;
24
+ treasury: string;
25
+ amount: string;
26
+ expiredAt: string;
27
+ jobId?: string;
28
+ agentName?: string;
29
+ evidence?: readonly EvaluationEvidence[];
29
30
  }
30
-
31
- function printPlan(label: string, plan: ContractCallPlan): void {
32
- console.log(`\n${label}`);
33
- console.log(
34
- json({
35
- network: plan.network,
36
- contract: plan.contract,
37
- functionName: plan.functionName,
38
- intent: plan.intent,
39
- postConditionMode: plan.postConditionMode,
40
- postConditionCount: plan.postConditions.length,
41
- })
42
- );
31
+ function ensure(ok: boolean, reason: string): asserts ok { if (!ok) throw new Error(reason); }
32
+ function loadRun(path: string): RunConfig {
33
+ ensure(isAbsolute(path), "absolute_config_required");
34
+ const input = JSON.parse(readFileSync(path, "utf8")) as RunConfig;
35
+ ensure(input.network === "testnet" && ["stx", "sbtc"].includes(input.asset), "testnet_only");
36
+ ensure(["client", "provider"].includes(input.role), "invalid_role");
37
+ ensure([input.client, input.provider, input.evaluator, input.treasury].every(value => /^ST[A-Z0-9]{20,41}$/.test(value)) &&
38
+ new Set([input.client, input.provider, input.evaluator, input.treasury]).size === 4, "distinct_testnet_roles_required");
39
+ ensure(/^[1-9][0-9]*$/.test(input.amount) && BigInt(input.amount) <= (input.asset === "sbtc" ? 1000n : 100000n), "test_amount_cap");
40
+ ensure(/^[1-9][0-9]*$/.test(input.expiredAt), "expiry_required");
41
+ ensure(input.jobId === undefined || /^[1-9][0-9]*$/.test(input.jobId), "invalid_job_id");
42
+ return input;
43
43
  }
44
-
45
- function dryRun(): void {
46
- const preview = new PerkOSClient({
47
- network: NETWORK,
48
- spendingPolicy: {
49
- allowedNetworks: [NETWORK],
50
- allowedAssets: ["sbtc"],
51
- maxPerTransaction: { sbtc: DEMO_AMOUNT },
52
- maxPerSession: { sbtc: DEMO_AMOUNT },
53
- },
54
- });
55
- const expiresAt = 1_000_000n;
56
- const plans = [
57
- [
58
- "1. Register provider agent",
59
- preview.transactions.registerAgent({
60
- name: "PerkOS Testnet Provider",
61
- description: "Agent SDK transactional quickstart provider",
62
- wallet: DRY_RUN_PROVIDER,
63
- endpoints: [{ name: "mcp", url: "https://example.com/mcp" }],
64
- }),
65
- ],
66
- [
67
- "2. Create sBTC job with distinct provider and evaluator",
68
- preview.transactions.createJob({
69
- asset: "sbtc",
70
- provider: DRY_RUN_PROVIDER,
71
- evaluator: DRY_RUN_EVALUATOR,
72
- expiredAt: expiresAt,
73
- description: "Create a signed testnet lifecycle receipt",
74
- }),
75
- ],
76
- [
77
- "3. Set job budget",
78
- preview.transactions.setBudget({
79
- asset: "sbtc",
80
- jobId: DEMO_JOB_ID,
81
- amount: DEMO_AMOUNT,
82
- }),
83
- ],
84
- [
85
- "4. Fund exact sBTC escrow",
86
- preview.transactions.fundJob({
87
- asset: "sbtc",
88
- jobId: DEMO_JOB_ID,
89
- amount: DEMO_AMOUNT,
90
- sender: DRY_RUN_CLIENT,
91
- }),
92
- ],
93
- [
94
- "5. Provider submits deliverable",
95
- preview.transactions.submitWork({
96
- asset: "sbtc",
97
- jobId: DEMO_JOB_ID,
98
- deliverable: "ipfs:bafy-perkos-testnet-receipt",
99
- }),
100
- ],
101
- [
102
- "6. Evaluator releases escrow",
103
- preview.transactions.completeJob({
104
- asset: "sbtc",
105
- jobId: DEMO_JOB_ID,
106
- amount: DEMO_AMOUNT,
107
- recipient: DRY_RUN_PROVIDER,
108
- }),
109
- ],
110
- [
111
- "7. Client rates provider",
112
- preview.transactions.rateProvider({
113
- asset: "sbtc",
114
- jobId: DEMO_JOB_ID,
115
- score: 5n,
116
- comment: "Completed through the Agent SDK quickstart",
117
- }),
118
- ],
119
- ] as const;
120
-
121
- console.log("PerkOS sBTC lifecycle: safe testnet preview");
122
- console.log("No wallet, key, transaction, or network request was used.");
123
- for (const [label, plan] of plans) printPlan(label, plan);
124
- console.log("\nFunding policy decision");
125
- console.log(json(preview.preview(plans[3][1])));
126
- console.log(
127
- "\nTo broadcast, copy examples/testnet.env.example, fund all three testnet roles with fee STX, fund the client with testnet sBTC, and set PERKOS_CONFIRM_TESTNET_BROADCAST=yes."
128
- );
44
+ async function tip() {
45
+ const response = await fetch(API + "/v2/info", { redirect: "error", signal: AbortSignal.timeout(15000) });
46
+ ensure(response.ok, "testnet_unavailable");
47
+ const info = await response.json() as Record<string, unknown>;
48
+ ensure(info.network_id === 2147483648, "wrong_network");
49
+ const burn = info.burn_block_height;
50
+ ensure(typeof burn === "number" && Number.isSafeInteger(burn) && burn >= 0, "invalid_burn_height");
51
+ const stacks = info.stacks_tip_height;
52
+ ensure(typeof stacks === "number" && Number.isSafeInteger(stacks) && stacks >= 0, "invalid_stacks_height");
53
+ return { burn: BigInt(burn), stacks: BigInt(stacks) };
129
54
  }
130
-
131
- function requiredEnvironment(name: string): string {
132
- const value = process.env[name]?.trim();
133
- if (!value) throw new Error(`${name} is required for live testnet mode.`);
134
- return value;
55
+ function print(value: unknown) {
56
+ console.log(JSON.stringify(value, (_key, item: unknown) => typeof item === "bigint" ? item.toString() : item, 2));
135
57
  }
136
-
137
- function amountFromEnvironment(): bigint {
138
- const value = requiredEnvironment("PERKOS_AMOUNT");
139
- if (!/^\d+$/.test(value) || BigInt(value) === 0n) {
140
- throw new Error("PERKOS_AMOUNT must be a positive integer number of satoshis.");
58
+ async function main() {
59
+ const action = process.env.PERKOS_ACTION ?? "preview";
60
+ if (action === "preview") {
61
+ print({ mode: "offline-preview", network: "testnet", contracts, steps,
62
+ note: "Client creates/funds/assigns; provider submits actual evidence. The isolated evaluator records a decision; finalize waits for the appeal deadline. No signer or network used." });
63
+ return;
141
64
  }
142
- return BigInt(value);
143
- }
144
-
145
- function okUint(confirmation: TransactionConfirmation, label: string): bigint {
146
- const repr = confirmation.result?.repr;
147
- const match = repr?.match(/^\(ok u(\d+)\)$/);
148
- if (!match?.[1]) {
149
- throw new Error(`${label} did not return an (ok uint) result: ${repr ?? "missing"}.`);
65
+ ensure(steps.includes(action as Step), "unknown_action");
66
+ const step = action as Step;
67
+ ensure(process.env.PERKOS_CONFIRM_TESTNET_BROADCAST === "yes" || step === "status", "explicit_testnet_consent_required");
68
+ const run = loadRun(process.env.PERKOS_RUN_CONFIG ?? "");
69
+ const contract = run.asset === "stx" ? contracts.stxCommerce : contracts.sbtcCommerce;
70
+ const input = { ...run, contract };
71
+ const prepared = await prepareEvaluationJob(input);
72
+ const wallet = run.role === "client" ? run.client : run.provider;
73
+ const amount = BigInt(run.amount);
74
+ const id = run.jobId ? BigInt(run.jobId) : undefined;
75
+ const config = { network: "testnet" as const, apiUrl: API, contracts,
76
+ spendingPolicy: { allowedNetworks: ["testnet"] as const, allowedAssets: [run.asset],
77
+ maxPerTransaction: { [run.asset]: amount }, maxPerSession: { [run.asset]: amount } } };
78
+ const reader = new PerkOSClient(config);
79
+ const { burn, stacks } = await tip();
80
+ if (step === "status") {
81
+ ensure(id !== undefined, "job_id_required");
82
+ const job = await reader.getJob(run.asset, id);
83
+ const decision = await reader.getDecision(run.asset, id);
84
+ const escrow = await reader.getEscrowBalance(run.asset, id);
85
+ print({ job, decision, escrow, burn,
86
+ note: "A decision is not a payout. Confirm settlement transfers and reputation separately." });
87
+ return;
150
88
  }
151
- return BigInt(match[1]);
152
- }
153
-
154
- async function broadcastAndConfirm(
155
- label: string,
156
- client: PerkOSClient,
157
- action: () => Promise<TransactionReceipt>
158
- ): Promise<TransactionConfirmation> {
159
- console.log(`\n${label}`);
160
- const broadcast = await action();
161
- console.log(`Broadcast: ${broadcast.explorerUrl}`);
162
- const confirmation = await client.confirm(broadcast, {
163
- pollIntervalMs: 5_000,
164
- timeoutMs: 10 * 60_000,
165
- });
166
- console.log(`Confirmation: ${confirmation.status}`);
167
- if (confirmation.status !== "success") {
168
- throw new PerkOSError(
169
- "CONFIRMATION_FAILED",
170
- `${label} ended with ${confirmation.status}.`,
171
- { txid: confirmation.txid, result: confirmation.result }
172
- );
89
+ ensure(!["create", "set-budget", "fund", "assign", "finalize"].includes(step) || run.role === "client", "client_role_required");
90
+ ensure(step !== "submit" || run.role === "provider", "provider_role_required");
91
+ ensure(["register", "create"].includes(step) || id !== undefined, "job_id_required");
92
+ const acceptance: ServiceFeeAcceptance = { gross: amount, basisPoints: 200,
93
+ treasury: run.treasury, rejectionRefund: "net-after-evaluation" };
94
+ if (["fund", "submit"].includes(step)) ensure(
95
+ process.env.PERKOS_ACCEPT_SERVICE_FEE === "200bps-net-after-evaluation", "explicit_service_fee_consent_required");
96
+
97
+ async function currentJob() {
98
+ ensure(id !== undefined, "job_id_required");
99
+ const job = await reader.getJob(run.asset, id);
100
+ ensure(!!job && job.client === run.client && job.evaluator === run.evaluator &&
101
+ job.description === prepared.description && job.treasury === run.treasury, "job_snapshot_mismatch");
102
+ return job;
173
103
  }
174
- return confirmation;
175
- }
176
-
177
- function liveClient(
178
- signer: PerkOSSigner,
179
- apiUrl: string,
180
- amount?: bigint
181
- ): PerkOSClient {
182
- return new PerkOSClient({
183
- network: NETWORK,
184
- apiUrl,
185
- signer,
186
- ...(amount === undefined
187
- ? {}
188
- : {
189
- spendingPolicy: {
190
- allowedNetworks: [NETWORK],
191
- allowedAssets: ["sbtc"],
192
- maxPerTransaction: { sbtc: amount },
193
- maxPerSession: { sbtc: amount },
194
- },
195
- }),
196
- });
197
- }
198
-
199
- async function liveRun(): Promise<void> {
200
- const amount = amountFromEnvironment();
201
- const apiUrl = (process.env.PERKOS_API_URL?.trim() || DEFAULT_API_URL).replace(
202
- /\/+$/,
203
- ""
204
- );
205
- const clientSigner = new HeadlessSigner({
206
- network: NETWORK,
207
- apiUrl,
208
- privateKeyProvider: () => requiredEnvironment("PERKOS_CLIENT_PRIVATE_KEY"),
209
- });
210
- const providerSigner = new HeadlessSigner({
211
- network: NETWORK,
212
- apiUrl,
213
- privateKeyProvider: () => requiredEnvironment("PERKOS_PROVIDER_PRIVATE_KEY"),
214
- });
215
- const evaluatorSigner = new HeadlessSigner({
216
- network: NETWORK,
217
- apiUrl,
218
- privateKeyProvider: () => requiredEnvironment("PERKOS_EVALUATOR_PRIVATE_KEY"),
219
- });
220
- const [clientAddress, providerAddress, evaluatorAddress, tip] = await Promise.all([
221
- clientSigner.getAddress(),
222
- providerSigner.getAddress(),
223
- evaluatorSigner.getAddress(),
224
- fetchChainTip(apiUrl),
225
- ]);
226
- if (new Set([clientAddress, providerAddress, evaluatorAddress]).size !== 3) {
227
- throw new Error("Client, provider, and evaluator keys must control distinct addresses.");
104
+ if (step === "evaluate") {
105
+ const job = await currentJob();
106
+ ensure(job.status === "submitted" && job.provider === run.provider && job.reviewDeadline !== undefined, "job_not_submitted");
107
+ const committed = await prepareEvaluationSubmission({ ...input, jobId: run.jobId!, evidence: run.evidence ?? [] });
108
+ ensure(job.deliverable?.replace(/^0x/, "") === Buffer.from(committed.deliverable).toString("hex"), "evidence_mismatch");
109
+ // An explicit QA origin is required. No bearer token or evaluator key is used.
110
+ const endpoint = new URL(process.env.PERKOS_EVALUATOR_URL ?? "");
111
+ ensure(endpoint.protocol === "https:" && !endpoint.username && !endpoint.password &&
112
+ endpoint.pathname === "/" && !endpoint.search && !endpoint.hash, "https_evaluator_origin_required");
113
+ const response = await fetch(new URL("/v1/evaluations", endpoint), {
114
+ method: "POST", redirect: "error", signal: AbortSignal.timeout(120000),
115
+ headers: { "content-type": "application/json" },
116
+ body: JSON.stringify({ commitmentVersion: "1", evaluationId: await evaluationJobId({
117
+ network: "testnet", contract, jobId: run.jobId! }), network: "testnet", asset: run.asset, contract, jobId: run.jobId,
118
+ job: { client: run.client, provider: run.provider, evaluator: run.evaluator,
119
+ status: "submitted", description: job.description, reviewDeadlineBurn: job.reviewDeadline.toString() },
120
+ acceptanceCriteria: run.acceptanceCriteria, evidence: run.evidence }),
121
+ });
122
+ ensure(response.status === 202, "evaluation_not_admitted");
123
+ print({ admitted: true, evaluationId: await evaluationJobId({ network: "testnet", contract, jobId: run.jobId! }),
124
+ note: "Queued is not approved or paid. Poll the evaluation status and the on-chain job." });
125
+ return;
228
126
  }
229
127
 
230
- const client = liveClient(clientSigner, apiUrl, amount);
231
- const provider = liveClient(providerSigner, apiUrl);
232
- const evaluator = liveClient(evaluatorSigner, apiUrl);
233
- const expiresAt = tip + 1_000n;
234
- const run = Date.now().toString(36);
235
-
236
- console.log("PerkOS sBTC lifecycle: LIVE TESTNET MODE");
237
- console.log(
238
- json({
239
- network: NETWORK,
240
- amountSatoshis: amount,
241
- client: clientAddress,
242
- provider: providerAddress,
243
- evaluator: evaluatorAddress,
244
- expiresAt,
245
- })
246
- );
247
-
248
- await broadcastAndConfirm("1. Register provider agent", provider, () =>
249
- provider.registerAgent({
250
- name: `Testnet Provider ${run}`,
251
- description: "PerkOS Agent SDK transactional quickstart provider",
252
- wallet: providerAddress,
253
- endpoints: [{ name: "mcp", url: "https://example.com/mcp" }],
254
- })
255
- );
256
- const created = await broadcastAndConfirm("2. Create sBTC job", client, () =>
257
- client.createJob({
258
- asset: "sbtc",
259
- provider: providerAddress,
260
- evaluator: evaluatorAddress,
261
- expiredAt: expiresAt,
262
- description: `Agent SDK testnet lifecycle ${run}`,
263
- })
264
- );
265
- const jobId = okUint(created, "create-job");
266
- console.log(`Job ID: ${jobId}`);
267
-
268
- await broadcastAndConfirm("3. Set job budget", client, () =>
269
- client.setBudget({ asset: "sbtc", jobId, amount })
270
- );
271
- await broadcastAndConfirm("4. Fund exact sBTC escrow", client, () =>
272
- client.fundJob({ asset: "sbtc", jobId, amount })
273
- );
274
- await broadcastAndConfirm("5. Provider submits deliverable", provider, () =>
275
- provider.submitWork({
276
- asset: "sbtc",
277
- jobId,
278
- deliverable: `ipfs:perkos-${run}`,
279
- })
280
- );
281
- await broadcastAndConfirm("6. Evaluator releases escrow", evaluator, () =>
282
- evaluator.completeJob("sbtc", jobId)
283
- );
284
- await broadcastAndConfirm("7. Client rates provider", client, () =>
285
- client.rateProvider({
286
- asset: "sbtc",
287
- jobId,
288
- score: 5n,
289
- comment: "Completed through the Agent SDK quickstart",
290
- })
291
- );
292
-
293
- const [job, reputation] = await Promise.all([
294
- client.getJob("sbtc", jobId),
295
- client.getReputation(providerAddress),
296
- ]);
297
- console.log("\nFinal on-chain state");
298
- console.log(json({ job, reputation }));
299
- }
300
-
301
- if (process.env.PERKOS_CONFIRM_TESTNET_BROADCAST === "yes") {
302
- await liveRun();
303
- } else {
304
- dryRun();
128
+ const journalPath = process.env.PERKOS_JOURNAL ?? "";
129
+ ensure(isAbsolute(journalPath), "absolute_journal_required");
130
+ // Atomic directory lock across processes. A crash deliberately leaves it for manual reconciliation.
131
+ const lock = journalPath + ".lock";
132
+ mkdirSync(lock, { mode: 0o700 });
133
+ let journal: QuickstartJournal | undefined;
134
+ try {
135
+ journal = new QuickstartJournal(journalPath);
136
+ const key = step + ":" + (["register", "create"].includes(step) ? "new" : run.jobId!);
137
+ const intent = intentHash({ step, run, contract });
138
+ const activeJournal = journal;
139
+ const send = async () => {
140
+ if (step === "create") ensure(BigInt(run.expiredAt) > stacks, "expired_job");
141
+ if (!["register", "create"].includes(step)) {
142
+ const job = await currentJob();
143
+ ensure(job.budget === amount || step === "set-budget", "budget_mismatch");
144
+ if (step === "set-budget") ensure(job.status === "open", "job_not_open");
145
+ if (step === "fund") {
146
+ ensure(job.status === "open" && await reader.getEscrowBalance(run.asset, id!) === 0n, "escrow_not_empty");
147
+ const policy = await reader.getServiceFeePolicy(run.asset);
148
+ ensure(policy.configured && policy.basisPoints === 200 && policy.treasury === run.treasury, "fee_policy_mismatch");
149
+ }
150
+ if (step === "assign") ensure(job.status === "funded" && !job.provider, "job_not_assignable");
151
+ if (step === "submit") ensure(job.status === "funded" && job.provider === run.provider, "job_not_assigned_to_provider");
152
+ if (step === "finalize") {
153
+ const decision = await reader.getDecision(run.asset, id!);
154
+ ensure(job.status === "decision-pending" && !!decision && !decision.appealedBy &&
155
+ !decision.finalDecision && burn > decision.appealDeadline, "waiting_for_appeal_deadline_or_resolution");
156
+ }
157
+ }
158
+ const signerPath = process.env.PERKOS_SIGNER_MODULE ?? "";
159
+ ensure(isAbsolute(signerPath), "absolute_signer_module_required");
160
+ const module = await import(pathToFileURL(signerPath).href) as { signer?: PerkOSSigner };
161
+ const signer = module.signer;
162
+ ensure(!!signer && await signer.getAddress() === wallet, "signer_role_mismatch");
163
+ const client = new PerkOSClient({ ...config, signer: {
164
+ getAddress: () => signer.getAddress(),
165
+ signAndBroadcast: async plan => ({ txid: await checkpointedTransaction(
166
+ activeJournal, key, intent, async () => (await signer.signAndBroadcast(plan)).txid,
167
+ ) }),
168
+ } });
169
+ if (step === "register") return (await client.registerAgent({
170
+ name: run.agentName ?? "Nayori QA participant", description: "Controlled QA participant", wallet, endpoints: [],
171
+ })).txid;
172
+ if (step === "create") return (await client.createJob({
173
+ asset: run.asset, evaluator: run.evaluator, expiredAt: BigInt(run.expiredAt), description: prepared.description,
174
+ })).txid;
175
+ if (step === "set-budget") return (await client.setBudget({ asset: run.asset, jobId: id!, amount })).txid;
176
+ if (step === "fund") return (await client.fundJob({ asset: run.asset, jobId: id!, amount, serviceFeeAcceptance: acceptance })).txid;
177
+ if (step === "assign") return (await client.assignProvider({ asset: run.asset, jobId: id!, provider: run.provider })).txid;
178
+ if (step === "submit") {
179
+ const committed = await prepareEvaluationSubmission({ ...input, jobId: run.jobId!, evidence: run.evidence ?? [] });
180
+ return (await client.submitWork({ asset: run.asset, jobId: id!, deliverable: committed.deliverable,
181
+ serviceFeeAcceptance: acceptance })).txid;
182
+ }
183
+ return (await client.finalizeDecision(run.asset, id!)).txid;
184
+ };
185
+ // A prior signed attempt bypasses mutable preflight and only confirms its saved txid.
186
+ const prior = journal.lookup(key, intent);
187
+ const txid = prior ? await checkpointedTransaction(journal, key, intent, async () => {
188
+ throw new Error("unexpected_retry");
189
+ }) : await send();
190
+ const confirmation = await reader.confirm(txid, { timeoutMs: 120000, pollIntervalMs: 10000 });
191
+ print({ step, txid, status: confirmation.status, result: confirmation.result?.repr, block: confirmation.blockHeight });
192
+ ensure(confirmation.status === "success", "not_confirmed_success_do_not_resend");
193
+ } finally { journal?.close(); rmdirSync(lock); }
305
194
  }
195
+ main().catch(() => {
196
+ // Raw signer/network/schema errors can contain credentials. Details belong in operator diagnostics.
197
+ console.error("Quickstart stopped safely. Verify config, on-chain state and journal before retrying; never delete an ambiguous attempt to resend.");
198
+ process.exitCode = 1;
199
+ });
@@ -1,14 +1,12 @@
1
- # The transactional quickstart is a dry run unless this exact value is set.
2
- PERKOS_CONFIRM_TESTNET_BROADCAST=yes
3
-
4
- # Use three distinct testnet identities. Never commit real key material.
5
- PERKOS_CLIENT_PRIVATE_KEY=
6
- PERKOS_PROVIDER_PRIVATE_KEY=
7
- PERKOS_EVALUATOR_PRIVATE_KEY=
8
-
9
- # Amount is denominated in satoshis. The client must hold this testnet sBTC
10
- # plus enough testnet STX for transaction fees. The other roles also need STX.
11
- PERKOS_AMOUNT=100
12
-
13
- # Optional custom Stacks API endpoint.
14
- # PERKOS_API_URL=https://api.testnet.hiro.so
1
+ # QA source candidate only; not a declaration that npm 0.7.1 has these helpers.
2
+ # Preview does not load keys, configuration or network.
3
+ PERKOS_ACTION=preview
4
+ # Select one operation from register/create/set-budget/fund/assign/submit/evaluate/status/finalize.
5
+ # Read docs/TESTNET_QUICKSTART.md before enabling actions.
6
+ PERKOS_CONFIRM_TESTNET_BROADCAST=
7
+ PERKOS_RUN_CONFIG=
8
+ PERKOS_JOURNAL=
9
+ PERKOS_SIGNER_MODULE=
10
+ PERKOS_ACCEPT_SERVICE_FEE=
11
+ # Explicit HTTPS origin of the QA evaluator, only after its rollout gate passes.
12
+ PERKOS_EVALUATOR_URL=
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@perkos/agent-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.8.0-rc.1",
4
4
  "description": "TypeScript SDK for agent identity, escrow settlement, and reputation on Stacks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "nayori-mcp": "./dist/mcp/cli.js",
10
+ "nayori-custody": "./dist/custody/cli.js"
11
+ },
8
12
  "exports": {
9
13
  ".": {
10
14
  "types": "./dist/index.d.ts",
@@ -64,12 +68,14 @@
64
68
  "license": "MIT",
65
69
  "publishConfig": {
66
70
  "access": "public",
71
+ "tag": "next",
67
72
  "provenance": true
68
73
  },
69
74
  "engines": {
70
75
  "node": ">=20"
71
76
  },
72
77
  "dependencies": {
78
+ "@modelcontextprotocol/sdk": "1.30.0",
73
79
  "@stacks/transactions": "^7.6.0",
74
80
  "@x402/core": "^2.23.0"
75
81
  },