@acnlabs/paperclip-plugin-acn 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ACN Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @acnlabs/paperclip-plugin-acn
2
+
3
+ A [Paperclip](https://github.com/paperclipai/paperclip) plugin that connects ACN (Agent Collaboration Network) as the **identity, communication, and settlement** layer for agent organizations.
4
+
5
+ ## What it does
6
+
7
+ | Direction | Trigger | Action |
8
+ |-----------|---------|--------|
9
+ | ACN → Paperclip | `task.created` webhook | Create a Paperclip issue (`todo`) mirroring the ACN task |
10
+ | ACN → Paperclip | `task.accepted` webhook | Move issue to `in_progress`, post the assignee as a comment |
11
+ | ACN → Paperclip | `task.submitted` webhook | Move issue to `in_review`, prompt reviewer to open the ACN tab |
12
+ | ACN → Paperclip | `task.completed` webhook | Move issue to `done`, post settlement comment |
13
+ | ACN → Paperclip | `task.rejected` / `task.cancelled` | Move issue to `cancelled`, post reason |
14
+ | ACN → Paperclip | `participation.rejected` | Post rejection reason + resubmit-count comment |
15
+ | Paperclip → ACN | Issue **created** by a human (not by this plugin) | Create a corresponding ACN task in the configured subnet |
16
+ | Paperclip → ACN | Issue moved to `done` (and `Auto-approve` enabled) | Call `/tasks/:id/review` → approve & settle |
17
+ | Paperclip → ACN | Issue moved to `cancelled` | Call `/tasks/:id/review` → reject |
18
+ | UI | ACN tab on issue | Show task ID, status, reward, participants; approve / reject pending submission |
19
+
20
+ > **Note (v0.1):** the plugin does **not** create or sync Paperclip agents from ACN. The Paperclip Plugin SDK does not yet expose a dynamic-agent-creation capability, so each agent type is provisioned independently (see *Agent topology* below).
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ Paperclip (L2 Orchestration: issues, agents, runs)
26
+ ↕ plugin (this repo)
27
+ ACN (L1 Identity + Routing + L3 Settlement)
28
+ ```
29
+
30
+ The plugin runs inside Paperclip's plugin worker sandbox. It uses the official
31
+ [`acn-client`](https://www.npmjs.com/package/acn-client) TypeScript SDK to call
32
+ ACN's REST API, and consumes ACN's per-subnet **Org Harness** webhook to
33
+ receive task lifecycle events.
34
+
35
+ ### Agent topology
36
+
37
+ | Agent type | Lives in Paperclip? | Lives in ACN? | How tasks reach it |
38
+ |---|---|---|---|
39
+ | **Paperclip native** | yes | optional | Paperclip wakeup; if registered in ACN it can also receive ACN tasks via its ACN endpoint |
40
+ | **ACN-only solver** | no | yes | ACN A2A messaging / task pool; this plugin surfaces the task in Paperclip for human review only |
41
+ | **External ACN agent** | no | yes (different org) | ACN inter-subnet messaging |
42
+
43
+ ## Installation
44
+
45
+ ### Prerequisites
46
+
47
+ - A running Paperclip instance (self-hosted, with the plugin worker enabled)
48
+ - An ACN deployment you can reach from the Paperclip host
49
+ - An ACN **agent API key** (`acn_…`) with `task.write` scope
50
+ - An ACN **subnet** owned by that agent (the plugin will register itself as the subnet's Org Harness)
51
+ - A shared **HMAC secret** for signing harness webhook deliveries (any high-entropy random string, e.g. `openssl rand -hex 32`)
52
+
53
+ ### 1. Install into Paperclip
54
+
55
+ ```bash
56
+ paperclipai plugin install @acnlabs/paperclip-plugin-acn
57
+ ```
58
+
59
+ This pulls the latest release from npm and registers it with the local Paperclip instance. Alternatively, for a working-copy install (useful while developing the plugin itself):
60
+
61
+ ```bash
62
+ git clone https://github.com/acnlabs/paperclip-acn-plugin.git
63
+ cd paperclip-acn-plugin
64
+ npm install
65
+ npm run build # produces dist/manifest.js, dist/worker.js, dist/ui/index.js
66
+ paperclipai plugin install ./
67
+ ```
68
+
69
+ ### 2. Configure the plugin
70
+
71
+ Paperclip → **Instance Settings → Plugins → ACN**:
72
+
73
+ | Field | Required | Description |
74
+ |-------|----------|-------------|
75
+ | `acnBaseUrl` | yes | Base URL of the ACN instance (no trailing slash), e.g. `https://acn.agentplanet.io` |
76
+ | `paperclipBaseUrl` | **strongly recommended** | Publicly reachable base URL of **this** Paperclip instance (e.g. `https://app.paperclip.ai`). Used to construct the harness webhook URL ACN posts to. If omitted, the plugin still calls into ACN outbound (Paperclip → ACN direction works) but cannot register itself as a webhook target, so inbound ACN events will be lost. |
77
+ | `acnApiKeyRef` | yes | Secret reference to the ACN agent API key (`acn_…`). Resolved at runtime via Paperclip's secret provider. |
78
+ | `acnHarnessSecretRef` | **strongly recommended** | Secret reference to the HMAC-SHA256 secret shared with ACN. The plugin verifies every inbound webhook against `X-ACN-Signature: sha256=<hex>`. **Leave blank only in trusted dev environments** — without a secret anyone who can reach `/api/plugins/acnlabs.acn/webhooks/acn-events` can forge ACN events. |
79
+ | `acnSubnetId` | yes | The ACN subnet whose tasks this plugin syncs |
80
+ | `autoCreateIssues` | no (default `true`) | Auto-create Paperclip issues for new ACN tasks |
81
+ | `autoApproveOnDone` | no (default `false`) | When a Paperclip user moves an ACN-linked issue to `done`, automatically call `/review?approved=true` and release payment. Off by default — keeps a human in the loop. |
82
+
83
+ ### 3. Verify
84
+
85
+ On worker startup the plugin:
86
+
87
+ 1. Resolves the API key and harness secret
88
+ 2. PATCHes `/api/v1/subnets/:id/harness` to register itself as the Org Harness (URL + secret)
89
+ 3. Does a full pull of `status in (open, in_progress, submitted)` ACN tasks for the subnet and mirrors them as Paperclip issues
90
+ 4. Subscribes to `issue.created` / `issue.updated` events
91
+
92
+ Successful boot logs (visible in **Instance Settings → Plugins → ACN → Logs**):
93
+
94
+ ```
95
+ acn-plugin: registered harness { subnet_id: "...", webhook_url: "...", signed: true }
96
+ acn-plugin: setup complete { subnet_id: "..." }
97
+ ```
98
+
99
+ If `signed: false` appears, the HMAC secret was **not** configured — the plugin will accept unsigned webhooks. Fix `acnHarnessSecretRef` in production.
100
+
101
+ ## Usage
102
+
103
+ ### ACN task → Paperclip issue
104
+
105
+ When a task is created in the configured subnet, ACN posts `task.created` to the harness URL. The plugin verifies the HMAC signature, fetches full task details, then creates a Paperclip issue:
106
+
107
+ | ACN event | Paperclip issue status |
108
+ |---|---|
109
+ | `task.created` | `todo` |
110
+ | `task.accepted` | `in_progress` |
111
+ | `task.submitted` | `in_review` |
112
+ | `task.completed` | `done` |
113
+ | `task.rejected` / `task.cancelled` | `cancelled` |
114
+
115
+ Each transition is annotated with a comment summarising the event (assignee, settlement note, rejection reason, …).
116
+
117
+ ### Paperclip issue → ACN task
118
+
119
+ When a Paperclip user creates an issue **that did not originate from this plugin** (detected via `event.actorType` and `originKind`), the plugin creates a matching ACN task with `reward: "0"` and `deadline_hours: 168` (7 days). Tune these defaults by extending `handleIssueCreated()` in `src/worker.ts`.
120
+
121
+ ### Manual review (ACN tab)
122
+
123
+ Open any ACN-linked issue and click the **ACN** tab to:
124
+
125
+ - See the linked ACN task ID, status, and reward
126
+ - View each participant's submission content (fetched live from ACN)
127
+ - Approve or reject the pending submission with optional notes
128
+
129
+ Approve and Reject both call `POST /api/v1/tasks/:id/review` with the appropriate `approved` flag and `notes` payload.
130
+
131
+ ## Security model
132
+
133
+ - **Inbound**: every ACN webhook is verified with HMAC-SHA256 against `X-ACN-Signature: sha256=<hex>` using the configured secret. Signature mismatch → request dropped, request_id logged. The secret is resolved via `ctx.secrets.resolve()`, never embedded in config.
134
+ - **Outbound**: ACN API calls use `Authorization: Bearer <acn_api_key>`. The key is resolved via secret ref and only held in memory inside the plugin worker.
135
+ - **State scope**: the `taskId → issueId` map is stored under `scopeKind: "company"`, so each company has its own isolated mapping.
136
+ - **Echo loops**: `handleIssueCreated` and `handleIssueUpdated` both skip events where `event.actorType === "plugin"`, preventing the plugin's own writes from triggering follow-up ACN calls.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ npm run dev # tsc --watch (worker only)
142
+ npm run build # full build: tsc + esbuild UI bundle
143
+ npm run typecheck # tsc --noEmit
144
+ ```
145
+
146
+ The plugin is runtime-typed against `@paperclipai/plugin-sdk` and `acn-client`, both pulled from npm. To work against a local SDK checkout, point the `acn-client` dependency at a sibling working tree (e.g. `npm install ../acn/clients/typescript`) — but remember to revert before releasing.
147
+
148
+ ### End-to-end smoke
149
+
150
+ The `scripts/` folder ships three live integration probes against a running ACN + Paperclip pair:
151
+
152
+ ```bash
153
+ node scripts/provision-e2e.mjs # one-shot setup (bridge agent + subnet + secrets)
154
+ node scripts/e2e-lifecycle.mjs # PC issue ↔ ACN task full path todo → done
155
+ node scripts/e2e-acn-to-paperclip.mjs # external ACN task → PC mirror issue
156
+ node scripts/e2e-paperclip-to-acn.mjs # PC issue → ACN task + echo-loop guard
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT — ACN Labs
@@ -0,0 +1,17 @@
1
+ export declare const PLUGIN_ID = "acnlabs.acn";
2
+ export declare const PLUGIN_VERSION = "0.1.0";
3
+ export declare const WEBHOOK_KEYS: {
4
+ readonly acnEvents: "acn-events";
5
+ };
6
+ export declare const SLOT_IDS: {
7
+ readonly issueTab: "acn-issue-tab";
8
+ };
9
+ export declare const EXPORT_NAMES: {
10
+ readonly issueTab: "ACNIssueTab";
11
+ };
12
+ /** Plugin state keys. State is company-scoped (see `loadMap`/`saveMap`). */
13
+ export declare const STATE_KEYS: {
14
+ /** Map of ACN taskId → Paperclip issueId. */
15
+ readonly issueTaskMap: "issue-task-map";
16
+ };
17
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,gBAAgB,CAAC;AACvC,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,eAAO,MAAM,YAAY;;CAEf,CAAC;AAEX,eAAO,MAAM,QAAQ;;CAEX,CAAC;AAEX,eAAO,MAAM,YAAY;;CAEf,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,UAAU;IACrB,6CAA6C;;CAErC,CAAC"}
@@ -0,0 +1,17 @@
1
+ export const PLUGIN_ID = "acnlabs.acn";
2
+ export const PLUGIN_VERSION = "0.1.0";
3
+ export const WEBHOOK_KEYS = {
4
+ acnEvents: "acn-events",
5
+ };
6
+ export const SLOT_IDS = {
7
+ issueTab: "acn-issue-tab",
8
+ };
9
+ export const EXPORT_NAMES = {
10
+ issueTab: "ACNIssueTab",
11
+ };
12
+ /** Plugin state keys. State is company-scoped (see `loadMap`/`saveMap`). */
13
+ export const STATE_KEYS = {
14
+ /** Map of ACN taskId → Paperclip issueId. */
15
+ issueTaskMap: "issue-task-map",
16
+ };
17
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC;AACvC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,SAAS,EAAE,YAAY;CACf,CAAC;AAEX,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,QAAQ,EAAE,eAAe;CACjB,CAAC;AAEX,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ,EAAE,aAAa;CACf,CAAC;AAEX,4EAA4E;AAC5E,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,6CAA6C;IAC7C,YAAY,EAAE,gBAAgB;CACtB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Echo-loop guards for Paperclip → ACN handlers.
3
+ *
4
+ * When this plugin creates or updates a Paperclip issue in response to an
5
+ * ACN webhook, the resulting `issue.created` / `issue.updated` event would
6
+ * normally fan back out to our own listeners and trigger a redundant call
7
+ * to ACN (e.g. creating a duplicate task). We need to drop those events
8
+ * without depending on `taskIssueMap`, which is updated *after* the write
9
+ * lands and is therefore racy.
10
+ *
11
+ * Two signals are checked, in order:
12
+ *
13
+ * 1. `event.actorType === "plugin"` — set by the Paperclip host before the
14
+ * event is dispatched, so it is race-free.
15
+ * 2. `event.payload.originKind` starts with `plugin:<this-plugin-id>` —
16
+ * catches the rare case where another part of the system replays a
17
+ * plugin-authored event without preserving `actorType`.
18
+ */
19
+ export interface MinimalPluginEvent {
20
+ actorType?: "user" | "agent" | "system" | "plugin";
21
+ payload?: unknown;
22
+ }
23
+ /**
24
+ * Returns `true` when the event should be skipped because it was authored
25
+ * by this plugin (or another plugin instance).
26
+ */
27
+ export declare function shouldSkipPluginEcho(event: MinimalPluginEvent, pluginId: string): boolean;
28
+ //# sourceMappingURL=echo-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"echo-guard.d.ts","sourceRoot":"","sources":["../../src/lib/echo-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,GACf,OAAO,CAgBT"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Echo-loop guards for Paperclip → ACN handlers.
3
+ *
4
+ * When this plugin creates or updates a Paperclip issue in response to an
5
+ * ACN webhook, the resulting `issue.created` / `issue.updated` event would
6
+ * normally fan back out to our own listeners and trigger a redundant call
7
+ * to ACN (e.g. creating a duplicate task). We need to drop those events
8
+ * without depending on `taskIssueMap`, which is updated *after* the write
9
+ * lands and is therefore racy.
10
+ *
11
+ * Two signals are checked, in order:
12
+ *
13
+ * 1. `event.actorType === "plugin"` — set by the Paperclip host before the
14
+ * event is dispatched, so it is race-free.
15
+ * 2. `event.payload.originKind` starts with `plugin:<this-plugin-id>` —
16
+ * catches the rare case where another part of the system replays a
17
+ * plugin-authored event without preserving `actorType`.
18
+ */
19
+ /**
20
+ * Returns `true` when the event should be skipped because it was authored
21
+ * by this plugin (or another plugin instance).
22
+ */
23
+ export function shouldSkipPluginEcho(event, pluginId) {
24
+ if (event.actorType === "plugin")
25
+ return true;
26
+ const payload = event.payload;
27
+ const originKind = payload?.originKind;
28
+ // Require the trailing `:` separator (or exact match) so that a plugin
29
+ // named `acnlabs.acn` does not also swallow events emitted by some other
30
+ // plugin called `acnlabs.acnplus`. Acceptable shapes:
31
+ // plugin:<pluginId>
32
+ // plugin:<pluginId>:<entity-kind>
33
+ if (originKind) {
34
+ const prefix = `plugin:${pluginId}`;
35
+ if (originKind === prefix || originKind.startsWith(`${prefix}:`))
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ //# sourceMappingURL=echo-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"echo-guard.js","sourceRoot":"","sources":["../../src/lib/echo-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAyB,EACzB,QAAgB;IAEhB,IAAI,KAAK,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE9C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAqD,CAAC;IAC5E,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,CAAC;IACvC,uEAAuE;IACvE,yEAAyE;IACzE,sDAAsD;IACtD,sBAAsB;IACtB,oCAAoC;IACpC,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,UAAU,QAAQ,EAAE,CAAC;QACpC,IAAI,UAAU,KAAK,MAAM,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IAChF,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve a plugin config field that may hold either:
3
+ * 1. A UUID-shaped Paperclip secret reference (resolved via the host) — used
4
+ * once Paperclip enables company-scoped plugin secret refs.
5
+ * 2. A literal value (e.g. an ACN agent api_key or a hex-encoded HMAC secret)
6
+ * — used today, because the current Paperclip build hard-disables secret
7
+ * refs in plugin config (`PLUGIN_SECRET_REFS_DISABLED_MESSAGE`).
8
+ *
9
+ * This dual mode lets operators move forward without waiting for the upstream
10
+ * "company-scoped plugin config" milestone and keeps the same code path live
11
+ * once that lands — only the config value format flips.
12
+ *
13
+ * Decision is made purely on the **shape** of the ref string, never on
14
+ * runtime errors, so a typo never silently leaks plaintext to the worker.
15
+ */
16
+ export declare function looksLikeSecretRef(value: string): boolean;
17
+ export interface SecretsResolver {
18
+ resolve(ref: string): Promise<string>;
19
+ }
20
+ /**
21
+ * @param ref The config value (UUID secret ref OR literal).
22
+ * @param secrets The plugin host's secrets adapter (`ctx.secrets`).
23
+ *
24
+ * If `ref` is UUID-shaped, asks the host to resolve it.
25
+ * Otherwise returns it verbatim.
26
+ */
27
+ export declare function resolveSecretOrLiteral(ref: string, secrets: SecretsResolver): Promise<string>;
28
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAKjB"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Resolve a plugin config field that may hold either:
3
+ * 1. A UUID-shaped Paperclip secret reference (resolved via the host) — used
4
+ * once Paperclip enables company-scoped plugin secret refs.
5
+ * 2. A literal value (e.g. an ACN agent api_key or a hex-encoded HMAC secret)
6
+ * — used today, because the current Paperclip build hard-disables secret
7
+ * refs in plugin config (`PLUGIN_SECRET_REFS_DISABLED_MESSAGE`).
8
+ *
9
+ * This dual mode lets operators move forward without waiting for the upstream
10
+ * "company-scoped plugin config" milestone and keeps the same code path live
11
+ * once that lands — only the config value format flips.
12
+ *
13
+ * Decision is made purely on the **shape** of the ref string, never on
14
+ * runtime errors, so a typo never silently leaks plaintext to the worker.
15
+ */
16
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ export function looksLikeSecretRef(value) {
18
+ return UUID_RE.test(value);
19
+ }
20
+ /**
21
+ * @param ref The config value (UUID secret ref OR literal).
22
+ * @param secrets The plugin host's secrets adapter (`ctx.secrets`).
23
+ *
24
+ * If `ref` is UUID-shaped, asks the host to resolve it.
25
+ * Otherwise returns it verbatim.
26
+ */
27
+ export async function resolveSecretOrLiteral(ref, secrets) {
28
+ if (looksLikeSecretRef(ref)) {
29
+ return secrets.resolve(ref);
30
+ }
31
+ return ref;
32
+ }
33
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,OAAO,GACX,iEAAiE,CAAC;AAEpE,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAMD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,GAAW,EACX,OAAwB;IAExB,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * HMAC-SHA256 signature verification for inbound ACN harness webhooks.
3
+ *
4
+ * ACN signs every harness webhook delivery with
5
+ * `hmac_sha256(secret, raw_json_body).hexdigest()` and sends it in the
6
+ * `X-ACN-Signature: sha256=<hex>` request header (see
7
+ * `acn/protocols/ap2/webhook.py::_sign_payload`). The plugin worker is
8
+ * solely responsible for verifying this signature — the Paperclip host
9
+ * does not inspect plugin webhook bodies.
10
+ */
11
+ /**
12
+ * Verify the `X-ACN-Signature` header against `rawBody` using the shared
13
+ * harness secret. Behaviour:
14
+ *
15
+ * - `secret === null` → returns `true` (no secret configured; operator has
16
+ * already been warned at setup time). Use this mode only in trusted dev
17
+ * environments.
18
+ * - header missing / malformed → returns `false`.
19
+ * - signature does not match → returns `false`.
20
+ * - signature matches → returns `true`.
21
+ *
22
+ * Constant-time comparison via {@link timingSafeEqual} prevents byte-by-byte
23
+ * timing oracles.
24
+ */
25
+ export declare function verifyAcnSignature(headers: Record<string, string | string[]>, rawBody: string, secret: string | null): boolean;
26
+ //# sourceMappingURL=signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/lib/signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAC1C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAkBT"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * HMAC-SHA256 signature verification for inbound ACN harness webhooks.
3
+ *
4
+ * ACN signs every harness webhook delivery with
5
+ * `hmac_sha256(secret, raw_json_body).hexdigest()` and sends it in the
6
+ * `X-ACN-Signature: sha256=<hex>` request header (see
7
+ * `acn/protocols/ap2/webhook.py::_sign_payload`). The plugin worker is
8
+ * solely responsible for verifying this signature — the Paperclip host
9
+ * does not inspect plugin webhook bodies.
10
+ */
11
+ import { createHmac, timingSafeEqual } from "node:crypto";
12
+ /**
13
+ * Verify the `X-ACN-Signature` header against `rawBody` using the shared
14
+ * harness secret. Behaviour:
15
+ *
16
+ * - `secret === null` → returns `true` (no secret configured; operator has
17
+ * already been warned at setup time). Use this mode only in trusted dev
18
+ * environments.
19
+ * - header missing / malformed → returns `false`.
20
+ * - signature does not match → returns `false`.
21
+ * - signature matches → returns `true`.
22
+ *
23
+ * Constant-time comparison via {@link timingSafeEqual} prevents byte-by-byte
24
+ * timing oracles.
25
+ */
26
+ export function verifyAcnSignature(headers, rawBody, secret) {
27
+ if (!secret)
28
+ return true;
29
+ const headerKey = Object.keys(headers).find((k) => k.toLowerCase() === "x-acn-signature");
30
+ const raw = headerKey ? headers[headerKey] : undefined;
31
+ const value = Array.isArray(raw) ? raw[0] : raw;
32
+ if (!value)
33
+ return false;
34
+ const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
35
+ const got = value.startsWith("sha256=") ? value.slice(7) : value;
36
+ if (got.length !== expected.length)
37
+ return false;
38
+ try {
39
+ return timingSafeEqual(Buffer.from(got, "hex"), Buffer.from(expected, "hex"));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ //# sourceMappingURL=signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.js","sourceRoot":"","sources":["../../src/lib/signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA0C,EAC1C,OAAe,EACf,MAAqB;IAErB,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CACzC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,iBAAiB,CAC7C,CAAC;IACF,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAChD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAEzB,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpF,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACjE,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACjD,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
2
+ declare const manifest: PaperclipPluginManifestV1;
3
+ export default manifest;
4
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AASzE,QAAA,MAAM,QAAQ,EAAE,yBAyGf,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,97 @@
1
+ import { EXPORT_NAMES, PLUGIN_ID, PLUGIN_VERSION, SLOT_IDS, WEBHOOK_KEYS, } from "./constants.js";
2
+ const manifest = {
3
+ id: PLUGIN_ID,
4
+ apiVersion: 1,
5
+ version: PLUGIN_VERSION,
6
+ displayName: "ACN — Agent Collaboration Network",
7
+ description: "Connect Paperclip to ACN: sync tasks, manage agent identity, and settle work through the ACN protocol layer.",
8
+ author: "acnlabs",
9
+ categories: ["connector", "automation"],
10
+ entrypoints: {
11
+ worker: "dist/worker.js",
12
+ ui: "dist/ui",
13
+ },
14
+ capabilities: [
15
+ "companies.read",
16
+ "issues.read",
17
+ "issues.create",
18
+ "issues.update",
19
+ "issue.comments.read",
20
+ "issue.comments.create",
21
+ "agents.read",
22
+ "events.subscribe",
23
+ "webhooks.receive",
24
+ "http.outbound",
25
+ "secrets.read-ref",
26
+ "plugin.state.read",
27
+ "plugin.state.write",
28
+ "ui.detailTab.register",
29
+ "instance.settings.register",
30
+ ],
31
+ instanceConfigSchema: {
32
+ type: "object",
33
+ properties: {
34
+ acnBaseUrl: {
35
+ type: "string",
36
+ title: "ACN Base URL",
37
+ default: "https://acn.agentplanet.io",
38
+ description: "Base URL of the ACN instance (no trailing slash).",
39
+ },
40
+ paperclipBaseUrl: {
41
+ type: "string",
42
+ title: "Paperclip Base URL",
43
+ default: "",
44
+ description: "Public base URL of this Paperclip instance (e.g. https://app.paperclip.ai). Used to construct the ACN harness webhook URL.",
45
+ },
46
+ acnApiKeyRef: {
47
+ type: "string",
48
+ title: "ACN API Key (secret ref)",
49
+ default: "",
50
+ description: "Secret reference to the ACN agent API key used to call ACN on behalf of Paperclip.",
51
+ },
52
+ acnHarnessSecretRef: {
53
+ type: "string",
54
+ title: "ACN Harness Webhook Secret (secret ref)",
55
+ default: "",
56
+ description: "Secret reference to the shared HMAC-SHA256 secret used to sign and verify ACN harness webhook deliveries (X-ACN-Signature). Leave blank to skip verification (NOT recommended in production).",
57
+ },
58
+ acnSubnetId: {
59
+ type: "string",
60
+ title: "ACN Subnet ID",
61
+ default: "",
62
+ description: "The ACN subnet whose tasks this plugin syncs into Paperclip issues.",
63
+ },
64
+ autoCreateIssues: {
65
+ type: "boolean",
66
+ title: "Auto-create Paperclip issues for new ACN tasks",
67
+ default: true,
68
+ },
69
+ autoApproveOnDone: {
70
+ type: "boolean",
71
+ title: "Auto-approve ACN task when Paperclip issue moves to 'done'",
72
+ default: false,
73
+ description: "When disabled, a board member must click Approve in the ACN tab to release payment.",
74
+ },
75
+ },
76
+ },
77
+ webhooks: [
78
+ {
79
+ endpointKey: WEBHOOK_KEYS.acnEvents,
80
+ displayName: "ACN Task Events",
81
+ description: "Receives HMAC-signed lifecycle events from ACN (task.created, task.submitted, task.completed, ...).",
82
+ },
83
+ ],
84
+ ui: {
85
+ slots: [
86
+ {
87
+ type: "detailTab",
88
+ id: SLOT_IDS.issueTab,
89
+ displayName: "ACN",
90
+ exportName: EXPORT_NAMES.issueTab,
91
+ entityTypes: ["issue"],
92
+ },
93
+ ],
94
+ },
95
+ };
96
+ export default manifest;
97
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,SAAS,EACT,cAAc,EACd,QAAQ,EACR,YAAY,GACb,MAAM,gBAAgB,CAAC;AAExB,MAAM,QAAQ,GAA8B;IAC1C,EAAE,EAAE,SAAS;IACb,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,cAAc;IACvB,WAAW,EAAE,mCAAmC;IAChD,WAAW,EACT,8GAA8G;IAChH,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC;IAEvC,WAAW,EAAE;QACX,MAAM,EAAE,gBAAgB;QACxB,EAAE,EAAE,SAAS;KACd;IAED,YAAY,EAAE;QACZ,gBAAgB;QAChB,aAAa;QACb,eAAe;QACf,eAAe;QACf,qBAAqB;QACrB,uBAAuB;QACvB,aAAa;QACb,kBAAkB;QAClB,kBAAkB;QAClB,eAAe;QACf,kBAAkB;QAClB,mBAAmB;QACnB,oBAAoB;QACpB,uBAAuB;QACvB,4BAA4B;KAC7B;IAED,oBAAoB,EAAE;QACpB,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,cAAc;gBACrB,OAAO,EAAE,4BAA4B;gBACrC,WAAW,EAAE,mDAAmD;aACjE;YACD,gBAAgB,EAAE;gBAChB,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,oBAAoB;gBAC3B,OAAO,EAAE,EAAE;gBACX,WAAW,EACT,4HAA4H;aAC/H;YACD,YAAY,EAAE;gBACZ,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,0BAA0B;gBACjC,OAAO,EAAE,EAAE;gBACX,WAAW,EACT,oFAAoF;aACvF;YACD,mBAAmB,EAAE;gBACnB,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,yCAAyC;gBAChD,OAAO,EAAE,EAAE;gBACX,WAAW,EACT,+LAA+L;aAClM;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,eAAe;gBACtB,OAAO,EAAE,EAAE;gBACX,WAAW,EACT,qEAAqE;aACxE;YACD,gBAAgB,EAAE;gBAChB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,gDAAgD;gBACvD,OAAO,EAAE,IAAI;aACd;YACD,iBAAiB,EAAE;gBACjB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,4DAA4D;gBACnE,OAAO,EAAE,KAAK;gBACd,WAAW,EACT,qFAAqF;aACxF;SACF;KACF;IAED,QAAQ,EAAE;QACR;YACE,WAAW,EAAE,YAAY,CAAC,SAAS;YACnC,WAAW,EAAE,iBAAiB;YAC9B,WAAW,EACT,qGAAqG;SACxG;KACF;IAED,EAAE,EAAE;QACF,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,WAAW;gBACjB,EAAE,EAAE,QAAQ,CAAC,QAAQ;gBACrB,WAAW,EAAE,KAAK;gBAClB,UAAU,EAAE,YAAY,CAAC,QAAQ;gBACjC,WAAW,EAAE,CAAC,OAAO,CAAC;aACvB;SACF;KACF;CACF,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
2
+ export interface AcnTaskInfo {
3
+ task_id: string;
4
+ title: string;
5
+ status: string;
6
+ /** Decimal string from ACN backend (e.g. "10.00"). */
7
+ reward: string;
8
+ reward_currency: string;
9
+ participations: Array<{
10
+ participation_id: string;
11
+ agent_id: string;
12
+ status: string;
13
+ submission_content: string | null;
14
+ submitted_at: string | null;
15
+ resubmit_count: number;
16
+ }>;
17
+ }
18
+ export declare function ACNIssueTab({ context }: PluginDetailTabProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAIpC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,KAAK,CAAC;QACpB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;QAClC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;CACJ;AAyPD,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE,oBAAoB,2CAuF5D"}