@ambitresearch/paperclip-agent-identities 0.2.3

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 Roshan Gautam
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,287 @@
1
+ # Agent Identities
2
+
3
+ Per-agent identity providers and contribution tools for Paperclip. GitHub is the first provider.
4
+
5
+ ## Features
6
+
7
+ - Configure one identity per Paperclip agent and provider pair.
8
+ - Choose from a provider registry: GitHub is enabled now; Slack, Mattermost, Microsoft Entra, Google Cloud, and AWS are tracked as coming soon.
9
+ - Prevent duplicate identities for the same agent/provider pair.
10
+ - Leave repository/resource access decisions to provider permissions such as GitHub App installations and scopes.
11
+ - Create GitHub Apps with GitHub's App Manifest flow from the settings page.
12
+ - Store public identity metadata in plugin state; GitHub credential references in a local sidecar; and Slack typed secret refs in company-scoped host config.
13
+ - Mint short-lived GitHub App installation tokens on demand for provider-specific tools.
14
+
15
+ ## Development
16
+
17
+ ```bash
18
+ pnpm install
19
+ pnpm dev # watch builds
20
+ pnpm dev:ui # local dev server with hot-reload events
21
+ pnpm typecheck
22
+ pnpm test
23
+ pnpm build
24
+ pnpm pack --pack-destination .
25
+ pnpm docs:dev # preview OpenWiki docs with VitePress
26
+ ```
27
+
28
+ `pnpm dev` rebuilds the worker, manifest, and UI bundles into `dist/`.
29
+ When this package is installed from a local path, Paperclip watches that rebuilt
30
+ output and reloads the plugin worker. Local installs run trusted code from this
31
+ folder on your machine.
32
+
33
+ ## Install Into Paperclip
34
+
35
+ ```bash
36
+ pnpm build
37
+ paperclipai plugin install . --local
38
+ ```
39
+
40
+ ### Required Paperclip host support for Slack
41
+
42
+ The Slack setup and Events API paths require a Paperclip host that implements
43
+ the company-scoped plugin contracts used by this branch:
44
+
45
+ - webhook delivery through
46
+ `/api/companies/<companyId>/plugins/<pluginId>/webhooks/<endpointKey>`, with
47
+ the route-derived `companyId` passed to the worker and the worker's HTTP
48
+ status, headers, and body returned to Slack;
49
+ - company-scoped plugin config reads and atomic secret-reference patches; and
50
+ - company-scoped secret resolution for the configured bot-token and signing-secret refs;
51
+ - `ctx.events.emit(name, companyId, payload)` plus fresh company invocation
52
+ scope on `plugin.<pluginId>.<name>` handlers and agent-session event
53
+ callbacks. Slack ingress uses a provider-owned `slack-turn-drain` self-event
54
+ to move session calls out of the webhook invocation.
55
+
56
+ For a dispatchable Slack event, the webhook verifies the signature and route,
57
+ persists a bounded turn in the per-conversation plugin-state queue, awaits the
58
+ self-event emit, and then returns HTTP 200. It never calls
59
+ `ctx.agents.sessions.sendMessage` or waits for a prior run in webhook scope. Queue state
60
+ retains up to 32 active/pending turns per conversation, hashes event IDs, and keeps completed
61
+ claims for 24 hours from completion. A full queue or failed self-event emit is
62
+ retryable before acknowledgement.
63
+
64
+ This path is designed to fit Slack's three-second acknowledgement window, but
65
+ the actual latency still includes host config/secret/state RPCs and the awaited
66
+ event emit. Those host operations must themselves remain available and timely.
67
+
68
+ The self-event drains one turn under fresh company scope. Accepted runs carry a
69
+ durable 30-minute lease; only a later fresh webhook/self-event or a terminal
70
+ session callback may finalize its accepted run; expired-run retirement and
71
+ session close happen only under a later fresh webhook/self-event scope. Generic
72
+ `sendMessage` failures are ambiguous because the host has no request-key API:
73
+ the provider marks the turn uncertain, retires the session, completes the event
74
+ claim, and never auto-resends. Only the host's definitive `Session not found`
75
+ response is safe to retry on a replacement session. This prevents duplicate
76
+ runs but cannot provide exactly-once delivery beyond that host boundary.
77
+
78
+ A worker restart is recoverable when a duplicate or new webhook arrives and
79
+ re-kicks the persisted queue. If the worker restarts after acknowledgement and
80
+ no later webhook/event arrives, queued work has no trigger; closing that gap
81
+ requires host-backed durable event scheduling or a request-key/idempotency API.
82
+ Likewise, a failed successor emit after terminal finalization leaves that
83
+ successor durable but waiting for the next duplicate/new webhook trigger.
84
+ Plugin state also has no compare-and-set primitive, so claim-token read-back
85
+ detects observable write races but does not make multi-worker execution atomic.
86
+
87
+ Slack install metadata and secret refs live under `identities.<agentId>.slack`, so the same
88
+ agent's existing flat GitHub instance config remains intact when Slack is saved or deleted.
89
+
90
+ Upgrades from released `v0.1.7`/`v0.1.8` may still have a legacy
91
+ `identities.<agentId>:slack.slackBotToken` entry in the local sidecar. Settings
92
+ shows **Rebind required** for that identity. Open **Edit**, select the signing
93
+ secret's Paperclip company-secret UUID when the released entry does not already
94
+ contain one, then choose **Rebind released credentials**. The worker verifies
95
+ the host-authorized company and agent membership, copies only typed UUID refs to
96
+ `identities.<agentId>.slack`, and never resolves either secret value. A matching
97
+ existing binding makes the retry idempotent; a conflicting binding is rejected.
98
+ If sidecar deletion fails after binding, Settings reports **Cleanup pending** and
99
+ the same action safely retries only cleanup. Reinstalling the Slack App is not
100
+ required for this migration.
101
+
102
+ The stock `2026.707.0` host does not provide those server-side contracts. The
103
+ pnpm patch in this repository updates the plugin worker SDK boundary, but it
104
+ does not make an unmodified Paperclip server compatible. Install this plugin
105
+ only with a host build containing the matching core support.
106
+
107
+ ## Identity Config Model
108
+
109
+ Agent Identities uses a provider-aware settings state. Each saved identity is keyed by `agentId + provider`, using the identity key format `${agentId}:${provider}`. The settings page stores a version 4 map in Paperclip plugin state:
110
+
111
+ ```ts
112
+ {
113
+ version: 4,
114
+ identities: Record<`${agentId}:${provider}`, AgentIdentityConfig>
115
+ }
116
+ ```
117
+
118
+ Core fields:
119
+
120
+ - `id`: stable identity key, for example `agent-123:github`
121
+ - `agentId`: Paperclip agent ID
122
+ - `provider`: provider ID such as `github`
123
+ - `label`: human-facing label, conventionally `Agent Name [Company Name]`
124
+ - `github.username`: GitHub App login for GitHub identities, commonly `<app-slug>[bot]`
125
+ - Optional `github.commitName` and `github.commitEmail`
126
+ - `slack.teamId`, `slack.appId`, and `slack.botUserId`: public Slack installation metadata
127
+
128
+ Each provider projects its own version 4 identity records into runtime config by `agentId`. Repository and channel access remains controlled by provider permissions and API responses, not by Agent Identities.
129
+
130
+ ### Supported providers
131
+
132
+ | Provider | Status | Notes |
133
+ | --- | --- | --- |
134
+ | GitHub | Enabled | GitHub App identity for repositories, pull requests, branch pushes, and commit attribution. |
135
+ | Slack | Coming soon | Workspace identity for Slack messages and app-mediated actions. |
136
+ | Mattermost | Coming soon | Team identity for posts and channel operations. |
137
+ | Microsoft Entra | Coming soon | Directory identity for Microsoft Graph and Azure-backed workflows. |
138
+ | Google Cloud | Coming soon | Service account identity for Google Cloud APIs. |
139
+ | AWS | Coming soon | IAM-backed identity for AWS APIs. |
140
+
141
+ ### GitHub App credentials
142
+
143
+ The settings page stores public identity metadata in plugin state, writes credential references to an operator-local sidecar file for the plugin worker, and cascades GitHub App bindings into the selected agent environment. Prefer GitHub App credentials so tools mint short-lived installation tokens just in time instead of reading generated token files:
144
+
145
+ ```json
146
+ {
147
+ "version": 1,
148
+ "identities": {
149
+ "<agent-id>:github": {
150
+ "githubApp": {
151
+ "appId": "<github-app-id>",
152
+ "installationId": "<github-installation-id>",
153
+ "privateKeySecretId": "<paperclip-company-secret-uuid-containing-private-key>",
154
+ "privateKeyFile": "<runtime-home>/.paperclip/agent-identities/github-apps/<agent>/private-key.pem"
155
+ }
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ Default sidecar path: `<runtime-home>/.paperclip/agent-identities/credentials.json`, resolved with Node's `os.homedir()`. This is `/Users/<user>/.paperclip/...` for a native macOS run and remains `/paperclip/.paperclip/...` in the Paperclip container, whose runtime home is `/paperclip`. `PAPERCLIP_AGENT_IDENTITIES_CREDENTIALS` overrides the default when the worker host passes that environment variable (including tests and custom worker hosts); relative override values are resolved against the worker's current directory and reported to the settings UI as absolute paths. The plugin worker reads this sidecar when its GitHub provider tools need credentials. The saved agent environment receives `GITHUB_APP_ID`, `GITHUB_INSTALLATION_ID`, and either `GITHUB_APP_PRIVATE_KEY` as a Paperclip secret reference or `GITHUB_APP_PRIVATE_KEY_FILE` as a private-key file path. The plugin tries the configured private-key secret first and falls back to `privateKeyFile` when a file path is present. It uses those GitHub App credentials to mint a fresh installation token on each GitHub tool call; generated tokens are not stored.
162
+
163
+ The settings page includes a **Create GitHub App on GitHub** button for bootstrapping this credential source with GitHub's App Manifest flow. The generated manifest opens GitHub with the required permissions (`contents`, `pull_requests`, `issues`, and `workflows` as `write`), marks the app private, uses the selected agent dashboard as the GitHub App homepage, redirects back to the current settings URL for manifest conversion, configures a GitHub App `setup_url` plus `setup_on_update` for post-install and repository-selection callbacks, explicitly disables OAuth-on-install, and intentionally omits `hook_attributes` for the no-webhook case; GitHub rejects `hook_attributes: { "active": false }` even though the resulting error says the URL is missing. After GitHub creates the app, the callback returns to the settings page and restores the relevant identity form with the one-time code prefilled; if the browser loses that state, paste the returned callback URL or `code=...` value into the field manually. The plugin exchanges that one-time code, writes the returned PEM content to `github-apps/<agent-id>/private-key.pem` beside the sidecar credentials file, prefills the App ID, private key file, and GitHub App login fields, then sends the browser into the GitHub App installation flow. GitHub redirects back to the setup URL with `installation_id`, and the settings page restores the same form with Installation ID prefilled before saving. The generated private key file is the automatic credential source. Operators can also copy that PEM into a Paperclip secret and select its UUID to prefer secret resolution over the file fallback. When editing an agent that already has GitHub App credentials, the manifest creation CTA is treated as a replacement/rotation flow and is tucked behind a disclosure so normal edits focus on the existing App ID, Installation ID, and key source.
164
+
165
+ Saving a GitHub identity patches the selected agent environment with the GitHub App bindings:
166
+
167
+ ```json
168
+ {
169
+ "adapterConfig": {
170
+ "env": {
171
+ "GITHUB_APP_ID": "<github-app-id>",
172
+ "GITHUB_INSTALLATION_ID": "<github-installation-id>",
173
+ "GITHUB_APP_PRIVATE_KEY": {
174
+ "type": "secret_ref",
175
+ "secretId": "<paperclip-company-secret-uuid-containing-private-key>",
176
+ "version": "latest"
177
+ }
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ Deleting an identity removes only matching GitHub App env bindings for that identity, preserving unrelated environment variables. `secretId`/`tokenFile` token fallback is still accepted, but GitHub App mode is the durable path.
184
+
185
+ ## Documentation site
186
+
187
+ OpenWiki-generated Markdown lives in [`openwiki/`](openwiki/quickstart.md). The repository publishes that content as a searchable VitePress site through GitHub Pages without moving OpenWiki's source folder.
188
+
189
+ ```bash
190
+ pnpm docs:dev # local VitePress server for openwiki/
191
+ pnpm docs:build # static site output in openwiki/.vitepress/dist
192
+ pnpm docs:preview # preview the built static site
193
+ ```
194
+
195
+ ## Build Options
196
+
197
+ - `pnpm build` uses esbuild presets from `@paperclipai/plugin-sdk/bundlers`.
198
+ - `pnpm build:rollup` uses rollup presets from the same SDK.
199
+
200
+ ## CI
201
+
202
+ GitHub Actions workflows: [CI](.github/workflows/ci.yml), [Release](.github/workflows/release.yml), [Publish](.github/workflows/publish.yml), and [Publish Docs](.github/workflows/pages.yml)
203
+
204
+ Runs on pull requests and pushes to `main`:
205
+
206
+ - `pnpm typecheck`
207
+ - `pnpm test`
208
+ - `pnpm build`
209
+ - `pnpm pack --pack-destination .`
210
+ - Uploads `*.tgz` as workflow artifact `npm-package-tarball`
211
+
212
+ ### Releases
213
+
214
+ Every push to `main` compares the merged `package.json` version with the previous commit. If the version is unchanged, the release workflow exits successfully. If it changed, the workflow requires a greater stable version, verifies that `src/manifest.ts` matches, validates the package, tags that exact merged SHA, creates the GitHub Release, and dispatches tag-based npm publication. Regular CI enforces package/manifest parity even when a release is skipped.
215
+
216
+ Version bumps are explicit normal pull-request changes. CI requires a canonical SemVer version greater than the base revision whenever `package.json` changes. Set the intended patch, minor, or major version in both `package.json` and `src/manifest.ts`; the merge is the release trigger. The workflow never increments versions or writes to `main`. Real npm publication accepts only a stable `v<major>.<minor>.<patch>` tag whose package and manifest versions match. `NPM_TOKEN` remains a repository secret used only by the publish workflow.
217
+
218
+
219
+ Run the same validation locally:
220
+
221
+ ```bash
222
+ corepack enable
223
+ corepack prepare pnpm@10.17.1 --activate
224
+ pnpm install --frozen-lockfile
225
+ pnpm typecheck
226
+ pnpm test
227
+ pnpm build
228
+ pnpm pack --pack-destination .
229
+ ```
230
+
231
+ Inspect package contents locally:
232
+
233
+ ```bash
234
+ TARBALL="$(ls -t ./*.tgz | head -n1)"
235
+ tar -tzf "$TARBALL"
236
+ ```
237
+
238
+ Download CI artifact from GitHub:
239
+
240
+ 1. Open the workflow run in the Actions tab.
241
+ 2. In the `Artifacts` section, download `npm-package-tarball`.
242
+ 3. Extract the `.tgz` and verify `dist/manifest.js`, `dist/worker.js`, and `dist/ui/index.js` are present.
243
+
244
+ ## Adding a provider
245
+
246
+ The plugin composes runtime identity-provider tools and actions behind a single
247
+ `IdentityProvider` contract (`src/core/provider-contract.ts`). Runtime
248
+ registration is generic: `src/worker.ts` and `src/manifest.ts` consume the
249
+ registry in `src/providers/index.ts`, so adding a provider's runtime tools does
250
+ not require a provider-specific registration branch in either file.
251
+
252
+ Settings persistence is a separate boundary and is not fully generic today.
253
+ Providers that can be created or edited in the settings UI must also extend the
254
+ persisted identity union and the appropriate credential-reference schema,
255
+ introduce or extend a provider-keyed settings-normalizer dispatch in
256
+ `src/worker.ts`, and add the corresponding UI form/projection. `src/manifest.ts` remains provider-agnostic;
257
+ `src/worker.ts` changes only at the settings-persistence boundary, not to
258
+ register runtime tools or actions.
259
+
260
+ To add a provider:
261
+
262
+ 1. Create a new module under `src/providers/<id>/` that implements `IdentityProvider<TIdentity, TRef>`.
263
+ 2. Write `validateConfig` to parse a single projected identity and return either the typed identity or a joined error string (see `validateGitHubConfig` in `src/providers/github/index.ts` for the pattern).
264
+ 3. If the provider is editable in settings, extend the persistence
265
+ discriminated union and credential-reference schema, add its provider-keyed normalization adapter to the
266
+ worker's settings dispatch, and add its UI form/projection. A runtime-only
267
+ provider can skip this step.
268
+ 4. Project raw settings-state identities for your provider's key (`${agentId}:<id>`) into that typed identity shape.
269
+ 5. Implement credential resolution (`resolveCredential`) — resolve secrets/tokens just in time, never eagerly, and never before params/identity/resource-ref have been validated.
270
+ 6. Provide `tools`: an array of `ProviderToolSpec` entries, each declaring its metadata, whether it requires a credential, and its `perform` implementation.
271
+ 7. Optionally contribute `actions` (e.g. an App-manifest-style setup flow) if the provider needs additional worker actions beyond tool calls.
272
+ 8. Include `manifestTools`: the manifest-facing fragments the composed manifest consumes (see `src/providers/github/manifest-tools.ts`).
273
+ 9. Append the new provider exactly once, in `src/providers/index.ts`'s `ALL_PROVIDERS` array. This is the single runtime composition root; no provider-specific registration branch belongs in `worker.ts` or `manifest.ts`.
274
+ 10. Add contract tests (validate/project/resolveCredential) and pipeline tests (tool execution through `createProviderTool`) alongside the existing provider test suites, and extend `tests/provider-composition.spec.ts` if the new provider changes composed output.
275
+
276
+ ### Security order (all provider tools)
277
+
278
+ Every credentialed tool call runs through the shared pipeline in `src/core/tool-pipeline.ts` in this fixed order:
279
+
280
+ 1. **Validate params** — deny malformed input before any secret work.
281
+ 2. **Resolve identity** — fail closed on any error.
282
+ 3. **Resolve resource ref** — derive/validate the target and deny disallowed targets before a credential exists.
283
+ 4. **Resolve credentials** — the first point secret material is touched, only after all prior denials.
284
+ 5. **Perform** — the only provider-specific API/git step.
285
+ 6. **Redact** — strip the resolved token and any other secrets from the tool result before it is returned.
286
+
287
+ Provider authors implement steps 1, 3 (optional), 4, and 5; the pipeline enforces the ordering and step 6.