@agrentingai/paperclip-adapter 0.3.0 → 0.4.0

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.
@@ -1,6 +1,12 @@
1
1
  // server/src/client.ts
2
2
  var DEFAULT_TIMEOUT_MS = 3e4;
3
3
  var MAX_RETRIES = 3;
4
+ var NonRetryableAgrentingError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "NonRetryableAgrentingError";
8
+ }
9
+ };
4
10
  var AgrentingClient = class {
5
11
  baseUrl;
6
12
  apiKey;
@@ -47,17 +53,22 @@ var AgrentingClient = class {
47
53
  await new Promise((resolve) => setTimeout(resolve, delayMs));
48
54
  continue;
49
55
  }
50
- throw new Error(
56
+ throw new NonRetryableAgrentingError(
51
57
  `Agrenting API ${response.status}: ${text.slice(0, 500)}`
52
58
  );
53
59
  }
54
60
  const envelope = await response.json();
55
61
  if (Array.isArray(envelope.errors) && envelope.errors.length) {
56
- throw new Error(`API errors: ${envelope.errors.join(", ")}`);
62
+ throw new NonRetryableAgrentingError(
63
+ `API errors: ${envelope.errors.join(", ")}`
64
+ );
57
65
  }
58
66
  return envelope.data ?? envelope;
59
67
  } catch (err) {
60
68
  lastError = err instanceof Error ? err : new Error(String(err));
69
+ if (lastError instanceof NonRetryableAgrentingError) {
70
+ throw lastError;
71
+ }
61
72
  if (attempt < MAX_RETRIES) {
62
73
  clearTimeout(timer);
63
74
  const delayMs = Math.min(1e3 * 2 ** attempt, 3e4);
@@ -243,12 +254,23 @@ var AgrentingClient = class {
243
254
  async getAgentProfile(agentDid) {
244
255
  return this.request("GET", `/api/v1/agents/${encodeURIComponent(agentDid)}`);
245
256
  }
246
- /** Hire/bind an agent to your account.
247
- * Returns adapter config so Paperclip can auto-provision the agent.
257
+ /** Create a paid hiring for an agent.
258
+ * Agrenting requires a concrete task, capability, and offered price.
248
259
  */
249
- async hireAgent(agentDid, options = {}) {
250
- const body = {};
251
- if (options.pricingModel) body.pricing_model = options.pricingModel;
260
+ async hireAgent(agentDid, options) {
261
+ const body = {
262
+ task_description: options.taskDescription,
263
+ capability_requested: options.capabilityRequested,
264
+ price: String(options.price),
265
+ delivery_mode: options.deliveryMode ?? "output"
266
+ };
267
+ if (options.repoUrl) body.repo_url = options.repoUrl;
268
+ if (options.repoAccessToken) body.repo_access_token = options.repoAccessToken;
269
+ if (options.clientIdempotencyKey) {
270
+ body.client_idempotency_key = options.clientIdempotencyKey;
271
+ }
272
+ if (options.taskInput) body.task_input = options.taskInput;
273
+ if (options.clientMessage) body.client_message = options.clientMessage;
252
274
  return this.request(
253
275
  "POST",
254
276
  `/api/v1/agents/${encodeURIComponent(agentDid)}/hire`,
@@ -307,13 +329,11 @@ var AgrentingClient = class {
307
329
  );
308
330
  }
309
331
  /** Get messages for a hiring.
310
- * GET /api/v1/hirings/:id/messages
332
+ * The canonical API includes recent messages in GET /api/v1/hirings/:id.
311
333
  */
312
334
  async getHiringMessages(hiringId) {
313
- return this.request(
314
- "GET",
315
- `/api/v1/hirings/${hiringId}/messages`
316
- );
335
+ const hiring = await this.getHiring(hiringId);
336
+ return hiring.messages ?? [];
317
337
  }
318
338
  /** Retry a failed hiring.
319
339
  * POST /api/v1/hirings/:id/retry
@@ -335,6 +355,15 @@ var AgrentingClient = class {
335
355
  async getHiring(hiringId) {
336
356
  return this.request("GET", `/api/v1/hirings/${hiringId}`);
337
357
  }
358
+ /** Cancel an active hiring.
359
+ * POST /api/v1/hirings/:id/cancel
360
+ */
361
+ async cancelHiring(hiringId) {
362
+ return this.request(
363
+ "POST",
364
+ `/api/v1/hirings/${hiringId}/cancel`
365
+ );
366
+ }
338
367
  /** List hirings for the authenticated agent.
339
368
  * GET /api/v1/hirings
340
369
  */
@@ -342,9 +371,16 @@ var AgrentingClient = class {
342
371
  const params = new URLSearchParams();
343
372
  if (options.status) params.set("status", options.status);
344
373
  if (options.limit) params.set("limit", String(options.limit));
345
- if (options.offset) params.set("offset", String(options.offset));
374
+ if (options.offset) {
375
+ const perPage = options.limit ?? 20;
376
+ params.set("page", String(Math.floor(options.offset / perPage) + 1));
377
+ }
346
378
  const query = params.toString() ? `?${params}` : "";
347
- return this.request("GET", `/api/v1/hirings${query}`);
379
+ const result = await this.request(
380
+ "GET",
381
+ `/api/v1/hirings${query}`
382
+ );
383
+ return result.hirings;
348
384
  }
349
385
  /** List agents filtered by capability for auto-select.
350
386
  * GET /api/v1/agents?capability=X
@@ -357,6 +393,493 @@ var AgrentingClient = class {
357
393
  }
358
394
  };
359
395
 
396
+ // server/src/paperclip.ts
397
+ var type = "agrenting";
398
+ var label = "Agrenting";
399
+ var agentConfigurationDoc = `# agrenting agent configuration
400
+
401
+ Adapter: agrenting
402
+
403
+ Use when:
404
+ - A Paperclip agent should delegate each heartbeat to a remote agent hired from Agrenting.
405
+ - The operator has an Agrenting user API token and sufficient ledger balance.
406
+
407
+ Core fields:
408
+ - agrentingUrl (required): Agrenting base URL, normally https://agrenting.com
409
+ - apiKey (required, secret): Agrenting user API token (ap_...)
410
+ - agentDid (required): DID of the marketplace agent to hire
411
+ - capabilityRequested (optional): defaults to the first capability on the agent profile
412
+ - price (optional): defaults to the agent's current base price
413
+ - timeoutSec (optional): maximum time to poll a hiring, default 600
414
+ - pollIntervalMs (optional): hiring status poll interval, default 2000
415
+ - deliveryMode (optional): output by default; push requires repository access
416
+ - repoUrl (optional): repository URL used only when deliveryMode is push
417
+
418
+ Recommended API-key scopes:
419
+ - agents:discover, agents:read, hire:create, hirings:read, hirings:cancel
420
+ - add balance:read and artifacts:read when sharing the key with Apps/Claude
421
+ - deposits:create and account:read/account:write are optional elevated scopes
422
+ - set max_price_per_hire on the key to cap paid actions
423
+
424
+ Execution:
425
+ - Each Paperclip run creates one canonical Agrenting hiring.
426
+ - The Paperclip run id is sent as client_idempotency_key so safe retries deduplicate.
427
+ - Paperclip polls the hiring until it completes, fails, is cancelled, or times out.
428
+ - Push delivery can use a repository URL from Paperclip and an Agrenting-stored
429
+ GitHub token. The adapter never stores a repository token in agent config.
430
+ `;
431
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
432
+ "completed",
433
+ "failed",
434
+ "cancelled",
435
+ "disputed",
436
+ "refunded"
437
+ ]);
438
+ var DEFAULT_TIMEOUT_SEC = 600;
439
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
440
+ var MAX_TASK_DESCRIPTION_LENGTH = 5e3;
441
+ function asRecord(value) {
442
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
443
+ return null;
444
+ }
445
+ return value;
446
+ }
447
+ function nonEmpty(value) {
448
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
449
+ }
450
+ function positiveNumber(value, fallback) {
451
+ const parsed = typeof value === "number" ? value : Number(value);
452
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
453
+ }
454
+ function configFrom(raw) {
455
+ return {
456
+ agrentingUrl: nonEmpty(raw.agrentingUrl) ?? "https://agrenting.com",
457
+ apiKey: nonEmpty(raw.apiKey) ?? "",
458
+ agentDid: nonEmpty(raw.agentDid) ?? "",
459
+ capabilityRequested: nonEmpty(raw.capabilityRequested) ?? void 0,
460
+ price: nonEmpty(raw.price) ?? void 0,
461
+ timeoutSec: positiveNumber(raw.timeoutSec, DEFAULT_TIMEOUT_SEC),
462
+ pollIntervalMs: positiveNumber(
463
+ raw.pollIntervalMs,
464
+ DEFAULT_POLL_INTERVAL_MS
465
+ ),
466
+ deliveryMode: raw.deliveryMode === "push" ? "push" : "output",
467
+ repoUrl: nonEmpty(raw.repoUrl) ?? void 0
468
+ };
469
+ }
470
+ function taskDescriptionFrom(ctx) {
471
+ const context = ctx.context;
472
+ const title = nonEmpty(context.taskTitle) ?? nonEmpty(context.issueTitle) ?? nonEmpty(context.title);
473
+ const body = nonEmpty(context.paperclipTaskMarkdown) ?? nonEmpty(context.taskBody) ?? nonEmpty(context.taskDescription) ?? nonEmpty(context.issueDescription) ?? nonEmpty(context.prompt) ?? nonEmpty(context.input);
474
+ let description = [title, body].filter(Boolean).join("\n\n").trim();
475
+ if (!description) {
476
+ description = `Continue the assigned Paperclip work for ${ctx.agent.name}.`;
477
+ }
478
+ if (description.length <= MAX_TASK_DESCRIPTION_LENGTH) return description;
479
+ return `${description.slice(0, MAX_TASK_DESCRIPTION_LENGTH - 32)}
480
+
481
+ [truncated by Paperclip adapter]`;
482
+ }
483
+ function capabilityFrom(config, context, profile) {
484
+ return nonEmpty(config.capabilityRequested) ?? nonEmpty(context.capabilityRequested) ?? nonEmpty(context.capability) ?? profile.capabilities.find((capability) => capability.trim().length > 0) ?? null;
485
+ }
486
+ function priceFrom(config, context, profile) {
487
+ return nonEmpty(config.price) ?? nonEmpty(context.price) ?? nonEmpty(context.maxPrice) ?? nonEmpty(profile.base_price);
488
+ }
489
+ function repoUrlFrom(ctx) {
490
+ const workspace = asRecord(ctx.context.paperclipWorkspace);
491
+ return nonEmpty(configFrom(ctx.config).repoUrl) ?? nonEmpty(workspace?.repoUrl) ?? void 0;
492
+ }
493
+ function taskInputFrom(ctx) {
494
+ const input = {
495
+ paperclip_run_id: ctx.runId,
496
+ paperclip_agent_id: ctx.agent.id,
497
+ paperclip_company_id: ctx.agent.companyId
498
+ };
499
+ const mappings = [
500
+ ["paperclip_issue_id", ctx.context.issueId ?? ctx.context.taskId],
501
+ ["paperclip_project_id", ctx.context.projectId],
502
+ ["paperclip_wake_reason", ctx.context.wakeReason]
503
+ ];
504
+ for (const [key, value] of mappings) {
505
+ const normalized = nonEmpty(value);
506
+ if (normalized) input[key] = normalized;
507
+ }
508
+ const wake = asRecord(ctx.context.paperclipWake);
509
+ if (wake) input.paperclip_wake = wake;
510
+ return input;
511
+ }
512
+ function outputText(hiring) {
513
+ const output = hiring.task_output;
514
+ if (typeof output === "string" && output.trim()) return output;
515
+ const record = asRecord(output);
516
+ if (record) {
517
+ for (const key of ["result", "output", "text", "summary"]) {
518
+ const direct = nonEmpty(record[key]);
519
+ if (direct) return direct;
520
+ }
521
+ return JSON.stringify(record, null, 2);
522
+ }
523
+ return `Agrenting hiring ${hiring.id} completed.`;
524
+ }
525
+ function firstLine(value) {
526
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Agrenting hiring completed";
527
+ }
528
+ function sleep(ms) {
529
+ return new Promise((resolve) => setTimeout(resolve, ms));
530
+ }
531
+ function resultForFailure(hiring, message) {
532
+ return {
533
+ exitCode: 1,
534
+ signal: null,
535
+ timedOut: false,
536
+ errorMessage: message,
537
+ errorCode: `agrenting_hiring_${hiring.status}`,
538
+ provider: "agrenting",
539
+ biller: "agrenting",
540
+ sessionParams: { hiringId: hiring.id },
541
+ sessionDisplayId: hiring.id,
542
+ resultJson: {
543
+ hiringId: hiring.id,
544
+ status: hiring.status,
545
+ failedReason: hiring.failed_reason ?? null
546
+ }
547
+ };
548
+ }
549
+ async function executePaperclip(ctx) {
550
+ const config = configFrom(ctx.config);
551
+ if (!config.apiKey || !config.agentDid) {
552
+ return {
553
+ exitCode: 1,
554
+ signal: null,
555
+ timedOut: false,
556
+ errorCode: "agrenting_config_invalid",
557
+ errorMessage: "Agrenting requires apiKey and agentDid."
558
+ };
559
+ }
560
+ const client = new AgrentingClient(config);
561
+ try {
562
+ const profile = await client.getAgentProfile(config.agentDid);
563
+ const capability = capabilityFrom(config, ctx.context, profile);
564
+ const price = priceFrom(config, ctx.context, profile);
565
+ if (!capability) {
566
+ throw new Error(
567
+ "No capabilityRequested was configured and the Agrenting agent profile has no capabilities."
568
+ );
569
+ }
570
+ if (!price) {
571
+ throw new Error(
572
+ "No price was configured and the Agrenting agent profile has no base_price."
573
+ );
574
+ }
575
+ const taskDescription = taskDescriptionFrom(ctx);
576
+ await ctx.onLog(
577
+ "stdout",
578
+ `[agrenting] Hiring ${config.agentDid} for ${capability} at ${price}
579
+ `
580
+ );
581
+ const created = await client.hireAgent(config.agentDid, {
582
+ taskDescription,
583
+ capabilityRequested: capability,
584
+ price,
585
+ deliveryMode: config.deliveryMode,
586
+ clientIdempotencyKey: ctx.runId,
587
+ taskInput: taskInputFrom(ctx),
588
+ repoUrl: repoUrlFrom(ctx)
589
+ });
590
+ const hiringId = created.hiring.id;
591
+ await ctx.onMeta?.({
592
+ adapterType: type,
593
+ command: "POST /api/v1/agents/:did/hire",
594
+ context: { hiringId, agentDid: config.agentDid, capability, price }
595
+ });
596
+ await ctx.onLog(
597
+ "stdout",
598
+ `[agrenting] Hiring ${hiringId} created with status ${created.hiring.status}
599
+ `
600
+ );
601
+ const timeoutMs = (config.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
602
+ const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
603
+ const deadline = Date.now() + timeoutMs;
604
+ let hiring = created.hiring;
605
+ let lastStatus = hiring.status;
606
+ while (!TERMINAL_STATUSES.has(hiring.status) && Date.now() < deadline) {
607
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
608
+ hiring = await client.getHiring(hiringId);
609
+ if (hiring.status !== lastStatus) {
610
+ lastStatus = hiring.status;
611
+ await ctx.onLog(
612
+ "stdout",
613
+ `[agrenting] Hiring ${hiringId} is ${hiring.status}
614
+ `
615
+ );
616
+ }
617
+ }
618
+ if (!TERMINAL_STATUSES.has(hiring.status)) {
619
+ try {
620
+ await client.cancelHiring(hiringId);
621
+ await ctx.onLog(
622
+ "stderr",
623
+ `[agrenting] Hiring ${hiringId} timed out and was cancelled
624
+ `
625
+ );
626
+ } catch (cancelError) {
627
+ await ctx.onLog(
628
+ "stderr",
629
+ `[agrenting] Hiring ${hiringId} timed out; cancellation failed: ${cancelError instanceof Error ? cancelError.message : String(cancelError)}
630
+ `
631
+ );
632
+ }
633
+ return {
634
+ exitCode: null,
635
+ signal: null,
636
+ timedOut: true,
637
+ errorCode: "agrenting_hiring_timeout",
638
+ errorMessage: `Agrenting hiring ${hiringId} timed out after ${config.timeoutSec ?? DEFAULT_TIMEOUT_SEC}s.`,
639
+ provider: "agrenting",
640
+ biller: "agrenting",
641
+ sessionParams: { hiringId },
642
+ sessionDisplayId: hiringId
643
+ };
644
+ }
645
+ if (hiring.status !== "completed") {
646
+ return resultForFailure(
647
+ hiring,
648
+ hiring.failed_reason ?? `Agrenting hiring ended with status ${hiring.status}.`
649
+ );
650
+ }
651
+ const output = outputText(hiring);
652
+ await ctx.onLog("stdout", `${output}
653
+ `);
654
+ const numericPrice = Number(hiring.price ?? price);
655
+ return {
656
+ exitCode: 0,
657
+ signal: null,
658
+ timedOut: false,
659
+ provider: "agrenting",
660
+ biller: "agrenting",
661
+ billingType: "fixed",
662
+ costUsd: Number.isFinite(numericPrice) ? numericPrice : null,
663
+ sessionParams: { hiringId },
664
+ sessionDisplayId: hiringId,
665
+ summary: firstLine(output),
666
+ resultJson: {
667
+ hiringId,
668
+ status: hiring.status,
669
+ taskOutput: hiring.task_output ?? null
670
+ }
671
+ };
672
+ } catch (error) {
673
+ const message = error instanceof Error ? error.message : String(error);
674
+ const transient = /\b(429|5\d\d)\b|rate.?limit|temporar|timeout/i.test(message);
675
+ await ctx.onLog("stderr", `[agrenting] ${message}
676
+ `);
677
+ return {
678
+ exitCode: 1,
679
+ signal: null,
680
+ timedOut: false,
681
+ errorCode: "agrenting_hiring_failed",
682
+ errorFamily: transient ? "transient_upstream" : null,
683
+ errorMessage: message,
684
+ provider: "agrenting",
685
+ biller: "agrenting"
686
+ };
687
+ }
688
+ }
689
+ function summarizeChecks(checks) {
690
+ if (checks.some((check) => check.level === "error")) return "fail";
691
+ if (checks.some((check) => check.level === "warn")) return "warn";
692
+ return "pass";
693
+ }
694
+ async function testPaperclipEnvironment(ctx) {
695
+ const config = configFrom(ctx.config);
696
+ const checks = [];
697
+ let parsedUrl = null;
698
+ try {
699
+ parsedUrl = new URL(config.agrentingUrl);
700
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
701
+ parsedUrl = null;
702
+ }
703
+ } catch {
704
+ parsedUrl = null;
705
+ }
706
+ if (!parsedUrl) {
707
+ checks.push({
708
+ code: "agrenting_url_invalid",
709
+ level: "error",
710
+ message: "agrentingUrl must be an http:// or https:// URL."
711
+ });
712
+ }
713
+ if (!config.apiKey) {
714
+ checks.push({
715
+ code: "agrenting_api_key_missing",
716
+ level: "error",
717
+ message: "Agrenting requires a user API token.",
718
+ hint: "Create an ap_... token in Agrenting and store it as a Paperclip secret."
719
+ });
720
+ }
721
+ if (!config.agentDid) {
722
+ checks.push({
723
+ code: "agrenting_agent_did_missing",
724
+ level: "error",
725
+ message: "Agrenting requires agentDid."
726
+ });
727
+ }
728
+ if (!checks.some((check) => check.level === "error")) {
729
+ const client = new AgrentingClient(config);
730
+ let connectionOk = false;
731
+ try {
732
+ await client.listHirings({ limit: 1 });
733
+ connectionOk = true;
734
+ checks.push({
735
+ code: "agrenting_connection_ok",
736
+ level: "info",
737
+ message: "Connected to the authenticated Agrenting hiring API."
738
+ });
739
+ } catch (error) {
740
+ checks.push({
741
+ code: "agrenting_connection_failed",
742
+ level: "error",
743
+ message: "Could not access the authenticated Agrenting hiring API.",
744
+ detail: error instanceof Error ? error.message : String(error)
745
+ });
746
+ }
747
+ if (connectionOk) {
748
+ try {
749
+ const profile = await client.getAgentProfile(config.agentDid);
750
+ checks.push({
751
+ code: "agrenting_agent_profile_ok",
752
+ level: "info",
753
+ message: `Found Agrenting agent ${profile.name} (${profile.did}).`,
754
+ detail: `${profile.capabilities.length} capabilities; base price ${profile.base_price ?? "not reported"}.`
755
+ });
756
+ } catch (error) {
757
+ checks.push({
758
+ code: "agrenting_agent_profile_failed",
759
+ level: "error",
760
+ message: `Could not load Agrenting agent ${config.agentDid}.`,
761
+ detail: error instanceof Error ? error.message : String(error)
762
+ });
763
+ }
764
+ }
765
+ }
766
+ return {
767
+ adapterType: ctx.adapterType,
768
+ status: summarizeChecks(checks),
769
+ checks,
770
+ testedAt: (/* @__PURE__ */ new Date()).toISOString()
771
+ };
772
+ }
773
+ function getPaperclipConfigSchema() {
774
+ return {
775
+ fields: [
776
+ {
777
+ key: "agrentingUrl",
778
+ label: "Agrenting URL",
779
+ type: "text",
780
+ required: true,
781
+ default: "https://agrenting.com",
782
+ hint: "Base URL of the Agrenting platform."
783
+ },
784
+ {
785
+ key: "apiKey",
786
+ label: "API token",
787
+ type: "text",
788
+ required: true,
789
+ hint: "Agrenting user API token (ap_...). Stored as a Paperclip secret.",
790
+ meta: { secret: true }
791
+ },
792
+ {
793
+ key: "agentDid",
794
+ label: "Agent DID",
795
+ type: "text",
796
+ required: true,
797
+ hint: "Marketplace agent DID, for example did:agrenting:code-reviewer."
798
+ },
799
+ {
800
+ key: "capabilityRequested",
801
+ label: "Capability",
802
+ type: "text",
803
+ hint: "Optional default. Falls back to the first capability on the agent profile."
804
+ },
805
+ {
806
+ key: "price",
807
+ label: "Price per hiring (USD)",
808
+ type: "text",
809
+ hint: "Optional. Falls back to the agent's current base price."
810
+ },
811
+ {
812
+ key: "timeoutSec",
813
+ label: "Timeout seconds",
814
+ type: "number",
815
+ default: DEFAULT_TIMEOUT_SEC
816
+ },
817
+ {
818
+ key: "pollIntervalMs",
819
+ label: "Poll interval milliseconds",
820
+ type: "number",
821
+ default: DEFAULT_POLL_INTERVAL_MS
822
+ },
823
+ {
824
+ key: "repoUrl",
825
+ label: "Repository URL",
826
+ type: "text",
827
+ hint: "Optional push target. Push delivery uses a GitHub token already stored in Agrenting."
828
+ },
829
+ {
830
+ key: "deliveryMode",
831
+ label: "Delivery mode",
832
+ type: "select",
833
+ default: "output",
834
+ options: [
835
+ { value: "output", label: "Return output" },
836
+ { value: "push", label: "Push to repository" }
837
+ ],
838
+ hint: "Use output unless the hiring also has repository credentials."
839
+ }
840
+ ]
841
+ };
842
+ }
843
+ var paperclipSessionCodec = {
844
+ deserialize(raw) {
845
+ if (typeof raw === "string") {
846
+ try {
847
+ return asRecord(JSON.parse(raw));
848
+ } catch {
849
+ return null;
850
+ }
851
+ }
852
+ return asRecord(raw);
853
+ },
854
+ serialize(params) {
855
+ return params;
856
+ },
857
+ getDisplayId(params) {
858
+ return nonEmpty(params?.hiringId);
859
+ },
860
+ encode(state) {
861
+ return JSON.stringify(state ?? null);
862
+ },
863
+ decode(blob) {
864
+ if (!blob) return null;
865
+ try {
866
+ return JSON.parse(blob);
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ };
872
+ function createCanonicalServerAdapter() {
873
+ return {
874
+ type,
875
+ execute: executePaperclip,
876
+ testEnvironment: testPaperclipEnvironment,
877
+ sessionCodec: paperclipSessionCodec,
878
+ getConfigSchema: getPaperclipConfigSchema,
879
+ agentConfigurationDoc
880
+ };
881
+ }
882
+
360
883
  // server/src/crypto.ts
361
884
  async function verifyWebhookSignature(rawBody, signature, secret) {
362
885
  const crypto = await import("crypto");
@@ -1092,9 +1615,9 @@ async function cancelTask(config, taskId) {
1092
1615
  var TASK_MAX_RETRIES = 2;
1093
1616
  var TASK_RETRY_BASE_DELAY_MS = 1e3;
1094
1617
  var TASK_RETRY_MAX_DELAY_MS = 3e4;
1095
- async function hireAgent(config, agentDid) {
1618
+ async function hireAgent(config, agentDid, options) {
1096
1619
  const client = new AgrentingClient(config);
1097
- return client.hireAgent(agentDid);
1620
+ return client.hireAgent(agentDid, options);
1098
1621
  }
1099
1622
  async function getAgentProfile(config, agentDid) {
1100
1623
  const client = new AgrentingClient(config);
@@ -1136,6 +1659,10 @@ async function listHirings(config, options) {
1136
1659
  const client = new AgrentingClient(config);
1137
1660
  return client.listHirings(options);
1138
1661
  }
1662
+ async function cancelHiring(config, hiringId) {
1663
+ const client = new AgrentingClient(config);
1664
+ return client.cancelHiring(hiringId);
1665
+ }
1139
1666
  async function autoSelectAgent(config, options) {
1140
1667
  const client = new AgrentingClient(config);
1141
1668
  const capabilities = await client.listCapabilities();
@@ -1161,7 +1688,8 @@ async function autoSelectAgent(config, options) {
1161
1688
  const minRep = options.minReputation;
1162
1689
  filtered = filtered.filter((a) => {
1163
1690
  if (!a.reputation_score) return false;
1164
- return a.reputation_score >= minRep;
1691
+ const reputation = Number(a.reputation_score);
1692
+ return Number.isFinite(reputation) && reputation >= minRep;
1165
1693
  });
1166
1694
  }
1167
1695
  if (filtered.length === 0) {
@@ -1172,15 +1700,17 @@ async function autoSelectAgent(config, options) {
1172
1700
  const sortBy = options.sortBy ?? "reputation_score";
1173
1701
  if (options.preferAvailable ?? true) {
1174
1702
  filtered.sort((a, b) => {
1175
- const aAvail = a.availability_status === "available" ? 0 : 1;
1176
- const bAvail = b.availability_status === "available" ? 0 : 1;
1703
+ const aAvail = (a.availability_status ?? a.availability ?? a.status) === "available" ? 0 : 1;
1704
+ const bAvail = (b.availability_status ?? b.availability ?? b.status) === "available" ? 0 : 1;
1177
1705
  if (aAvail !== bAvail) return aAvail - bAvail;
1178
1706
  return 0;
1179
1707
  });
1180
1708
  }
1181
1709
  filtered.sort((a, b) => {
1182
1710
  if (sortBy === "reputation_score") {
1183
- return (b.reputation_score ?? 0) - (a.reputation_score ?? 0);
1711
+ const aReputation = Number(a.reputation_score ?? 0);
1712
+ const bReputation = Number(b.reputation_score ?? 0);
1713
+ return (Number.isFinite(bReputation) ? bReputation : 0) - (Number.isFinite(aReputation) ? aReputation : 0);
1184
1714
  }
1185
1715
  if (sortBy === "base_price") {
1186
1716
  const aPrice = parseFloat(a.base_price ?? "999999");
@@ -1188,14 +1718,19 @@ async function autoSelectAgent(config, options) {
1188
1718
  return aPrice - bPrice;
1189
1719
  }
1190
1720
  if (sortBy === "availability") {
1191
- const aAvail = a.availability_status === "available" ? 0 : 1;
1192
- const bAvail = b.availability_status === "available" ? 0 : 1;
1721
+ const aAvail = (a.availability_status ?? a.availability ?? a.status) === "available" ? 0 : 1;
1722
+ const bAvail = (b.availability_status ?? b.availability ?? b.status) === "available" ? 0 : 1;
1193
1723
  return aAvail - bAvail;
1194
1724
  }
1195
1725
  return 0;
1196
1726
  });
1197
1727
  const selectedAgent = filtered[0];
1198
- const hireResult = await client.hireAgent(selectedAgent.did);
1728
+ const hireResult = await client.hireAgent(selectedAgent.did, {
1729
+ taskDescription: options.taskDescription,
1730
+ capabilityRequested: options.capability,
1731
+ price: selectedAgent.base_price ?? options.maxPrice ?? "0",
1732
+ deliveryMode: "output"
1733
+ });
1199
1734
  return {
1200
1735
  ...hireResult,
1201
1736
  selectedAgent
@@ -1291,19 +1826,7 @@ async function syncSkills(config, existing) {
1291
1826
  removed: existing.filter((s) => !remoteIds.has(s.id))
1292
1827
  };
1293
1828
  }
1294
- var sessionCodec = {
1295
- encode(state) {
1296
- return JSON.stringify(state ?? null);
1297
- },
1298
- decode(blob) {
1299
- if (!blob) return null;
1300
- try {
1301
- return JSON.parse(blob);
1302
- } catch {
1303
- return null;
1304
- }
1305
- }
1306
- };
1829
+ var sessionCodec = paperclipSessionCodec;
1307
1830
  async function invoke(config, params) {
1308
1831
  return execute(config, params);
1309
1832
  }
@@ -1322,11 +1845,15 @@ async function cancel(config, taskId) {
1322
1845
  }
1323
1846
  }
1324
1847
  function createServerAdapter() {
1848
+ const canonical = createCanonicalServerAdapter();
1325
1849
  return {
1850
+ ...canonical,
1326
1851
  name: "agrenting",
1327
- execute,
1328
- testEnvironment,
1329
- getConfigSchema,
1852
+ // Legacy helpers remain available under explicit names. The canonical
1853
+ // Paperclip keys above use AdapterExecutionContext and structured tests.
1854
+ legacyExecute: execute,
1855
+ legacyTestEnvironment: testEnvironment,
1856
+ getLegacyConfigSchema: getConfigSchema,
1330
1857
  startWebhookListener,
1331
1858
  stopWebhookListener,
1332
1859
  registerWebhook,
@@ -1344,6 +1871,7 @@ function createServerAdapter() {
1344
1871
  sendMessageToHiring,
1345
1872
  getHiringMessages,
1346
1873
  retryHiring,
1874
+ cancelHiring,
1347
1875
  getHiring,
1348
1876
  listHirings,
1349
1877
  autoSelectAgent,
@@ -1354,23 +1882,55 @@ function createServerAdapter() {
1354
1882
  getTransactions,
1355
1883
  deposit,
1356
1884
  withdraw,
1357
- // Canonical contract additions:
1885
+ // Legacy pre-0.4 contract additions:
1358
1886
  invoke,
1359
1887
  status,
1360
1888
  cancel,
1361
- detectModel,
1362
- listSkills,
1363
- syncSkills,
1364
- sessionCodec
1889
+ legacyDetectModel: detectModel,
1890
+ legacyListSkills: listSkills,
1891
+ legacySyncSkills: syncSkills
1365
1892
  };
1366
1893
  }
1894
+
1895
+ // server/src/apps-v2.ts
1896
+ var agrentingAppGalleryEntry = {
1897
+ key: "agrenting",
1898
+ name: "Agrenting",
1899
+ logoUrl: "https://www.google.com/s2/favicons?domain=agrenting.com&sz=64",
1900
+ tagline: "Discover and hire governed remote AI agents.",
1901
+ description: "Give Paperclip agents governed access to discover, hire, monitor, and cancel remote agents in the Agrenting marketplace.",
1902
+ authKind: "api_key",
1903
+ transportTemplate: {
1904
+ transport: "remote_http",
1905
+ url: "https://agrenting.com/mcp/hirer"
1906
+ },
1907
+ credentialFields: [
1908
+ {
1909
+ label: "Agrenting API key",
1910
+ configPath: "credentials.authorization",
1911
+ helpUrl: "https://agrenting.com/dashboard/api-keys",
1912
+ required: true,
1913
+ placement: "header",
1914
+ key: "Authorization",
1915
+ prefix: "Bearer "
1916
+ }
1917
+ ],
1918
+ recommendedDefaults: {
1919
+ access: "all_agents",
1920
+ askFirstRiskLevels: ["write", "destructive"]
1921
+ },
1922
+ urlPatterns: ["https://agrenting.com/mcp/hirer*"]
1923
+ };
1367
1924
  export {
1368
1925
  AgrentingClient,
1369
1926
  MAX_POLLS,
1370
1927
  POLL_INTERVALS_MS,
1928
+ agentConfigurationDoc,
1929
+ agrentingAppGalleryEntry,
1371
1930
  autoSelectAgent,
1372
1931
  canSubmitTask,
1373
1932
  cancel,
1933
+ cancelHiring,
1374
1934
  cancelTask,
1375
1935
  checkBalance,
1376
1936
  createServerAdapter,
@@ -1378,6 +1938,7 @@ export {
1378
1938
  deregisterWebhook,
1379
1939
  detectModel,
1380
1940
  execute,
1941
+ executePaperclip,
1381
1942
  executeWithRetry,
1382
1943
  formatAgentResponse,
1383
1944
  formatInsufficientBalanceComment,
@@ -1389,14 +1950,17 @@ export {
1389
1950
  getConfigSchema,
1390
1951
  getHiring,
1391
1952
  getHiringMessages,
1953
+ getPaperclipConfigSchema,
1392
1954
  getTaskMessages,
1393
1955
  getTaskProgress,
1394
1956
  getWebhookGracePeriodMs,
1395
1957
  hireAgent,
1396
1958
  invoke,
1959
+ label,
1397
1960
  listCapabilities,
1398
1961
  listHirings,
1399
1962
  listSkills,
1963
+ paperclipSessionCodec,
1400
1964
  pollTaskUntilDone,
1401
1965
  processIncomingMessage,
1402
1966
  reassignTask,
@@ -1409,6 +1973,8 @@ export {
1409
1973
  status,
1410
1974
  syncSkills,
1411
1975
  testEnvironment,
1976
+ testPaperclipEnvironment,
1977
+ type,
1412
1978
  unregisterTaskMapping,
1413
1979
  verifyWebhookSignature
1414
1980
  };