Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ With this plugin, you can:
- import open GitHub issues into Paperclip without adding title prefixes or duplicate issues
- keep descriptions, labels, and status aligned with GitHub over time
- configure mappings and import defaults per Paperclip company
- on authenticated Paperclip deployments, choose exactly which company agents should receive the saved GitHub token as `GITHUB_TOKEN`
- choose exactly which company agents should receive the saved GitHub token as `GITHUB_TOKEN`
- run sync manually or on a schedule
- triage open pull requests from mapped Paperclip projects in a hosted queue
- give Paperclip agents native GitHub tools for issues, pull requests, CI, review threads, and org-level projects
Expand All @@ -29,7 +29,7 @@ With this plugin, you can:
The plugin adds a full in-host workflow instead of a one-off import script:

- a hosted settings page for GitHub auth, repository mappings, company defaults, execution-policy handoff fallbacks, and sync controls
- authenticated-only setup controls for Paperclip board access and company-scoped agent token propagation
- setup controls for Paperclip board access and company-scoped agent token propagation
- a dashboard widget that shows sync readiness, current sync status, and run/cancel controls
- a separate KPI dashboard widget that tracks GitHub backlog size, GitHub issues closed, and Paperclip pull requests created with recent history and historical comparisons
- saved sync diagnostics that let operators inspect the latest per-issue failures, raw errors, and suggested next steps
Expand Down Expand Up @@ -137,8 +137,8 @@ npx paperclipai plugin install --local "$PWD"

1. Open the plugin settings for **GitHub Sync** from inside the Paperclip company you want to configure.
2. Paste a GitHub token, validate it, and save it.
3. If the deployment is authenticated, connect Paperclip board access from the same settings page and complete the approval flow.
4. If the deployment is authenticated, choose which agents in the current company should receive the saved GitHub token as `GITHUB_TOKEN`.
3. If the deployment is authenticated or local trusted, connect Paperclip board access from the same settings page and complete the approval flow when host API calls need board credentials.
4. Choose which agents in the current company should receive the saved GitHub token as `GITHUB_TOKEN`.
5. Add one or more repository mappings for the current company.
6. For each mapping, either choose an existing GitHub-linked Paperclip project or enter the project name that should receive synced issues.
7. Optionally configure company-wide defaults for imported issues, including the default assignee, the default Paperclip status, executor/reviewer/approver handoff assignees for sync-driven transitions, and ignored GitHub usernames. When Paperclip board access is connected, each assignee dropdown also offers `Me` for the connected board user. `Automatic routing` means GitHub Sync follows the issue's Paperclip execution policy first and only uses the saved fallback when Paperclip does not expose the next reviewer, approver, or return assignee yet. Bot aliases such as `renovate[bot]` are matched when you save `renovate`.
Expand Down Expand Up @@ -197,10 +197,10 @@ The plugin is designed to avoid persisting raw credentials in plugin state.
- GitHub tokens saved through the UI are stored as per-company Paperclip secret references.
- Paperclip board access tokens are also stored as per-company secret references.
- The settings UI also keeps lightweight non-secret identity labels for those saved connections, so later visits can still show who each company GitHub token and board access are connected as.
- On authenticated deployments, any selected propagation agents receive `GITHUB_TOKEN` as an agent env secret-ref binding that points at the same saved GitHub token secret instead of a copied raw token.
- Any selected propagation agents receive `GITHUB_TOKEN` as an agent env secret-ref binding that points at the same saved GitHub token secret instead of a copied raw token.
- The worker resolves those secret references at runtime instead of storing raw tokens in plugin state.
- When the current Paperclip host rejects plugin secret refs, GitHub Sync keeps company-scoped worker-local compatibility copies for GitHub tokens and Paperclip board-access tokens in `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json`. Reconnect board access once after upgrading if sync still cannot authenticate Paperclip label or issue REST calls.
- On authenticated Paperclip deployments, sync is blocked until the relevant company has connected Paperclip board access.
- On authenticated Paperclip deployments, sync is blocked until the relevant company has connected Paperclip board access. On local trusted deployments, board access setup remains visible so operators can configure it for host API paths that still require board credentials, but missing board access does not by itself block sync preflight.
- KPI API route requests must include `Authorization: Bearer <PAPERCLIP_API_KEY>` from an agent run; the Paperclip host authenticates the token and supplies the agent company before the worker records any metric event.

### Optional worker-local token file
Expand All @@ -225,7 +225,7 @@ Notes:
- The raw token is never persisted back into plugin state or plugin config.
- A GitHub token secret saved through the settings UI is the primary source. If the current Paperclip host rejects plugin secret-ref resolution while company-scoped plugin config is unavailable, GitHub Sync stores the validated token in `githubTokensByCompanyId` as a worker-local compatibility fallback.
- A Paperclip board access secret saved through the settings UI is also the primary source. If the host cannot resolve it for plugin workers, reconnecting board access stores the approved board token in `paperclipBoardApiTokensByCompanyId` as a worker-local compatibility fallback for direct Paperclip REST calls.
- On authenticated deployments, selected agents receive `GITHUB_TOKEN` as a latest-version secret-ref env binding, and the settings UI patches agent adapter config with `replaceAdapterConfig: true` so newer Paperclip hosts persist the merged env map.
- Selected agents receive `GITHUB_TOKEN` as a latest-version secret-ref env binding, and the settings UI patches agent adapter config with `replaceAdapterConfig: true` so newer Paperclip hosts persist the merged env map.

### Worker-facing Paperclip API URL

Expand Down
11 changes: 6 additions & 5 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ The plugin MUST provide a settings page inside Paperclip where an operator can c

- a GitHub token stored as a company-scoped Paperclip secret reference
- an optional external config file at `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` for worker-only global values such as a raw `githubToken`
- Paperclip board access, which is optional on unauthenticated deployments and required when the Paperclip deployment reports `deploymentMode: "authenticated"`
- on authenticated deployments, a company-scoped multi-select of agents that should receive `GITHUB_TOKEN` propagation from the saved GitHub token secret
- Paperclip board access, which is optional but configurable on `deploymentMode: "local_trusted"` deployments and required when the Paperclip deployment reports `deploymentMode: "authenticated"`
- a company-scoped multi-select of agents that should receive `GITHUB_TOKEN` propagation from the saved GitHub token secret
- one or more GitHub repository mappings
- company-scoped advanced defaults for imported issues: default assignee, default Paperclip status, executor/reviewer/approver handoff assignees for sync-driven status transitions, and ignored GitHub issue authors, where a saved username such as `renovate` also matches GitHub bot logins such as `renovate[bot]`
- the frequency for automatic scheduled sync runs for that company
Expand All @@ -21,7 +21,8 @@ The settings page MUST allow saving mappings and triggering a manual sync.
- The settings page SHOULD clearly label company-scoped setup and defaults, including sync cadence, for the active company.
- When a company context is present, the settings page SHOULD show the active company name prominently using a human-friendly label instead of a raw identifier.
- When a company already has Paperclip projects bound to GitHub repository workspaces, the settings page SHOULD surface those projects so an operator can enable sync without recreating the project.
- The settings page MUST only render the Paperclip board-access connect controls and the agent token-propagation selector when the current Paperclip deployment reports `deploymentMode: "authenticated"`.
- The settings page MUST render the Paperclip board-access connect controls when the current Paperclip deployment reports `deploymentMode: "authenticated"` or `deploymentMode: "local_trusted"`, while only treating board access as required for sync when the deployment reports `deploymentMode: "authenticated"`.
- The settings page MUST render the agent token-propagation selector regardless of deployment mode so selected agents can receive the saved `GITHUB_TOKEN` secret reference on local trusted and authenticated instances.
- When the settings page successfully validates a saved GitHub token, it SHOULD persist the validated GitHub login as non-secret display metadata so later visits can continue showing `Authenticated as ...` instead of falling back to a generic ready state.
- When the settings page successfully connects Paperclip board access for a company, it SHOULD persist a company-scoped non-secret identity label so later visits can continue showing `Connected as ...` instead of falling back to a generic connected state.
- When the active company has a saved Paperclip board-access user id, every advanced-settings assignee dropdown MUST include a `Me` option that maps to that connected board user alongside the available agents.
Expand All @@ -41,8 +42,8 @@ The settings page MUST allow saving mappings and triggering a manual sync.
- UI-side plugin config writes MUST recover from the known plugin-secret-reference-disabled failure by retrying without plugin secret-ref maps, so saving non-secret settings cannot be blocked by stale secret-ref config.
- If a Paperclip host rejects plugin secret-ref resolution with the known plugin-secret-reference-disabled failure while company-scoped plugin config is unavailable, the settings UI MAY ask the worker to persist the validated raw token in a worker-local company-scoped fallback map at `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` so production sync can continue without writing the raw token to plugin state or plugin config.
- The plugin MAY persist lightweight non-secret display metadata such as the validated GitHub login alongside the saved GitHub token secret ref so hosted UI can keep connected-state copy consistent across refreshes without resolving the secret.
- When authenticated deployment settings select agents for GitHub token propagation, the hosted settings UI MUST patch those agents through the host API so `adapterConfig.env.GITHUB_TOKEN` points at that same secret UUID with a latest-version secret-ref binding instead of copying the raw token value.
- When an authenticated deployment settings save removes an agent from that propagation allowlist, the hosted settings UI SHOULD remove `adapterConfig.env.GITHUB_TOKEN` only when that binding still points at the plugin-managed secret UUID, so unrelated manual agent env settings are not clobbered.
- When settings select agents for GitHub token propagation, the hosted settings UI MUST patch those agents through the host API so `adapterConfig.env.GITHUB_TOKEN` points at that same secret UUID with a latest-version secret-ref binding instead of copying the raw token value.
- When a settings save removes an agent from that propagation allowlist, the hosted settings UI SHOULD remove `adapterConfig.env.GITHUB_TOKEN` only when that binding still points at the plugin-managed secret UUID, so unrelated manual agent env settings are not clobbered.
- Agent token propagation updates MUST send `replaceAdapterConfig: true` when patching agent adapter config so newer Paperclip hosts persist the merged `env` map.
- If `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` exists and contains either a string `githubToken` or a `githubTokensByCompanyId` map, the worker MUST treat those values as worker-only fallback sources for the GitHub token without persisting or returning those raw tokens.
- The raw Paperclip board API token MUST NOT be persisted in plugin state.
Expand Down
10 changes: 7 additions & 3 deletions scripts/e2e/run-paperclip-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,8 @@ async function main() {
const manualIssueLinkIssue = await ensureUnlinkedSeedIssue(company, seededProject, manualGitHubIssueLinkTitle);
const manualPullRequestLinkIssue = await ensureUnlinkedSeedIssue(company, seededProject, manualGitHubPullRequestLinkTitle);
const health = await fetchJson(new URL('/api/health', baseUrl).toString());
const isAuthenticatedDeployment = String(health?.deploymentMode ?? '').toLowerCase() === 'authenticated';
const deploymentMode = String(health?.deploymentMode ?? '').toLowerCase();
const shouldShowBoardAccessSettings = deploymentMode === 'authenticated' || deploymentMode === 'local_trusted';

const { chromium } = await import('playwright');
const browser = await chromium.launch({ headless: true });
Expand Down Expand Up @@ -881,13 +882,16 @@ async function main() {

await page.getByRole('heading', { name: 'GitHub Sync' }).waitFor({ timeout: 120000 });
await page.getByText('GitHub Sync settings', { exact: true }).waitFor({ timeout: 120000 });
const settingsSurface = page.locator('.ghsync-settings');
await page.getByRole('heading', { name: 'GitHub access', exact: true }).waitFor({ timeout: 120000 });
const boardAccessHeading = page.getByRole('heading', { name: 'Paperclip board access', exact: true });
if (isAuthenticatedDeployment) {
if (shouldShowBoardAccessSettings) {
await boardAccessHeading.waitFor({ timeout: 120000 });
} else if (await boardAccessHeading.count() > 0) {
throw new Error('Paperclip board access settings should stay hidden outside authenticated deployments.');
throw new Error('Paperclip board access settings should stay hidden outside authenticated or local trusted deployments.');
}
await settingsSurface.getByRole('button', { name: 'Expand', exact: true }).click();
await page.getByLabel('Propagate GitHub token to agents').waitFor({ timeout: 120000 });
await page.getByRole('heading', { name: 'Repositories', exact: true }).waitFor({ timeout: 120000 });
await page.getByRole('heading', { name: 'Sync', exact: true }).waitFor({ timeout: 120000 });
await assertWorkerDoesNotReadPaperclipApiUrlFromRuntimeEnv(installedPluginId, company.id);
Expand Down
23 changes: 23 additions & 0 deletions src/paperclip-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ export interface PaperclipHealthResponse {
authReady?: boolean;
}

export interface PaperclipAuthControlsPolicy {
boardAccessRequired: boolean;
boardAccessSettingsVisible: boolean;
githubTokenPropagationSettingsVisible: boolean;
}

function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
Expand Down Expand Up @@ -31,11 +37,28 @@ export function normalizePaperclipHealthResponse(value: unknown): PaperclipHealt

export function requiresPaperclipBoardAccess(value: unknown): boolean {
const health = normalizePaperclipHealthResponse(value);
return requiresPaperclipBoardAccessForHealth(health);
}

function requiresPaperclipBoardAccessForHealth(health: PaperclipHealthResponse | null): boolean {
return health?.deploymentMode?.toLowerCase() === 'authenticated';
}

export function shouldShowPaperclipBoardAccessSettings(value: unknown): boolean {
const health = normalizePaperclipHealthResponse(value);
return shouldShowPaperclipBoardAccessSettingsForHealth(health);
}

function shouldShowPaperclipBoardAccessSettingsForHealth(health: PaperclipHealthResponse | null): boolean {
const deploymentMode = health?.deploymentMode?.toLowerCase();
return deploymentMode === 'authenticated' || deploymentMode === 'local_trusted';
}

export function resolvePaperclipAuthControlsPolicy(value: unknown): PaperclipAuthControlsPolicy {
const health = normalizePaperclipHealthResponse(value);
return {
boardAccessRequired: requiresPaperclipBoardAccessForHealth(health),
boardAccessSettingsVisible: shouldShowPaperclipBoardAccessSettingsForHealth(health),
githubTokenPropagationSettingsVisible: true
};
}
Loading