mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
144083fd481464f4a328f0d184a82263de63228d
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d295251550 |
feat(adapter-claude): add Claude Sonnet 5 to the static model fallback (#10280)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents pick their model from a dropdown in agent config, populated
per-adapter by `listAdapterModels()` → each adapter's live provider
catalog merged over a static fallback list
> - For `claude_local`, newer model ids only reach the dropdown via the
live Anthropic `/v1/models` fetch, which needs a server
`ANTHROPIC_API_KEY`, a <5s round-trip, non-Bedrock mode, and account
entitlement; on any miss it silently falls back to the static `models`
array
> - Claude Sonnet 5 (`claude-sonnet-5`) is a current flagship but was
absent from that static fallback, so it appeared only when live
discovery happened to succeed — i.e. "the newest model doesn't
consistently show up"
> - This pull request adds `claude-sonnet-5` to the `claude_local`
static model list so it is selectable regardless of the live-discovery
path
> - The benefit is a consistent, reliable dropdown that no longer
depends on a flaky live fetch to surface a shipped flagship model
## Linked Issues or Issue Description
No public GitHub issue. The bug is described inline following the
bug-report template:
**What happened**
The `claude_local` agent-config model dropdown intermittently omitted
Claude Sonnet 5. `claude-sonnet-5` was missing from the adapter's static
fallback `models` array (`packages/adapters/claude-local/src/index.ts`),
so it only surfaced when the live Anthropic `/v1/models` discovery
happened to succeed.
**Expected behavior**
Claude Sonnet 5 is a shipped flagship model and should always be
selectable in the dropdown, independent of whether live discovery
succeeds.
**Steps to reproduce**
1. Run the server without a working live Anthropic `/v1/models` path (no
`ANTHROPIC_API_KEY`, Bedrock mode, a discovery timeout, or a cache
miss).
2. Open agent config for a `claude_local` agent and inspect the model
dropdown.
3. Observe that `claude-sonnet-5` is absent because the static fallback
list omitted it.
**Deployment mode**
Self-hosted / local adapter (`claude_local`); the server process reads
`ANTHROPIC_API_KEY` from its environment.
## What Changed
- Added `{ id: "claude-sonnet-5", label: "Claude Sonnet 5" }` to the
`claude_local` static `models` fallback, immediately after
`claude-opus-4-8` (so Opus 4.8 stays the default first option).
- Added an explicit regression assertion in
`server/src/__tests__/adapter-models.test.ts` that `claude-sonnet-5` is
present in the `claude_local` fallback when live discovery is
unavailable.
## Verification
- `pnpm -C server exec vitest run src/__tests__/adapter-models.test.ts
-t "claude fallback"` — **passes** (the new `claude-sonnet-5` assertion
included).
- Reviewed the consuming tests: the fallback test also asserts
`models[0]?.id === "claude-opus-4-8"` (still index 0 — Sonnet 5 is index
1, unaffected); `adapter-registry.test.ts` reads `builtIn?.models`
dynamically, so no exact-array snapshot breaks.
- Change is a single static-data addition plus a test assertion; no
control-flow change.
## Risks
- Low risk. Pure additive change to a fallback list; no control-flow
change. Worst case is an id that a given account isn't entitled to,
which the existing "current"/manual-model UI paths already tolerate.
## Model Used
Claude (Anthropic), model id `claude-opus-4-8` (Opus 4.8), extended
thinking + tool use, run as the Paperclip CTO agent.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (branch is the assigned
execution-workspace branch and cannot be renamed this run)
- [x] I have run tests locally and they pass (server adapter-models
"claude fallback" case)
- [x] I have added or updated tests where applicable (explicit
`claude-sonnet-5` fallback assertion)
- [x] I have updated relevant documentation to reflect my changes (n/a —
no docs reference this list)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
|
||
|
|
4eace88f6b |
feat(adapter-claude): add Claude Opus 5 to the static model fallback (#10327)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents pick their model from a dropdown in agent config, populated
per-adapter by `listAdapterModels()` → each adapter's live provider
catalog merged over a static fallback list
> - For `claude_local`, newer model ids only reach the dropdown via the
live Anthropic `/v1/models` fetch, which needs a server
`ANTHROPIC_API_KEY`, a <5s round-trip, non-Bedrock mode, and account
entitlement; on any miss it silently falls back to the static `models`
array
> - Claude Opus 5 (`claude-opus-5`) is generally available — Anthropic
lists it as the recommended model for complex agentic coding and
enterprise work — but it was absent from that static fallback, so it
appeared only when live discovery happened to succeed
> - This pull request adds `claude-opus-5` to the `claude_local` static
model list so it is selectable regardless of the live-discovery path
> - The benefit is a consistent, reliable dropdown that surfaces the
current GA Opus flagship without depending on a flaky live fetch
## Linked Issues or Issue Description
No public GitHub issue. The bug is described inline following the
bug-report template:
**What happened**
The `claude_local` agent-config model dropdown omitted Claude Opus 5.
`claude-opus-5` was missing from the adapter's static fallback `models`
array (`packages/adapters/claude-local/src/index.ts`), so it only
surfaced when the live Anthropic `/v1/models` discovery happened to
succeed.
**Expected behavior**
Claude Opus 5 is a shipped, generally-available flagship (Anthropic's
recommended model for agentic coding) and should always be selectable in
the dropdown, independent of whether live discovery succeeds.
**Steps to reproduce**
1. Run the server without a working live Anthropic `/v1/models` path (no
`ANTHROPIC_API_KEY`, Bedrock mode, a discovery timeout, or a cache
miss).
2. Open agent config for a `claude_local` agent and inspect the model
dropdown.
3. Observe that `claude-opus-5` is absent because the static fallback
list omitted it.
**Deployment mode**
Self-hosted / local adapter (`claude_local`); the server process reads
`ANTHROPIC_API_KEY` from its environment.
## What Changed
- Added `{ id: "claude-opus-5", label: "Claude Opus 5" }` to the
`claude_local` static `models` fallback. Placed after the current
5-family entries and above the legacy `claude-opus-4-7`, so
`claude-opus-4-8` stays the default (index 0) option.
- Added an explicit regression assertion in
`server/src/__tests__/adapter-models.test.ts` that `claude-opus-5` is
present in the `claude_local` fallback when live discovery is
unavailable.
## Verification
- `pnpm -C server exec vitest run src/__tests__/adapter-models.test.ts`
— **17/17 pass**, including the new `claude-opus-5` assertion and the
existing `models[0] === "claude-opus-4-8"` default invariant (unaffected
— Opus 5 is inserted lower in the list).
- Change is a single static-data addition plus a test assertion; no
control-flow change.
## Risks
- Low risk. Pure additive change to a fallback list; no control-flow
change. Worst case is an id a given account isn't entitled to, which the
existing "current"/manual-model UI paths already tolerate.
- Note for reviewers: a sibling PR adds `claude-sonnet-5` to the same
static array (near `claude-opus-4-8`). Both are complementary "refresh
the static list to current GA" changes; whichever merges second may need
a one-line merge resolution in
`packages/adapters/claude-local/src/index.ts` and the matching test
assertion block.
## Model Used
Claude (Anthropic), model id `claude-opus-4-8` (Opus 4.8), extended
thinking + tool use, run as the Paperclip CTO agent.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (branch is the assigned
execution-workspace branch and cannot be renamed this run)
- [x] I have run tests locally and they pass (server adapter-models
suite, 17/17)
- [x] I have added or updated tests where applicable (explicit
`claude-opus-5` fallback assertion)
- [x] I have updated relevant documentation to reflect my changes (n/a —
no docs reference this list)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
|
||
|
|
caae2778f0 |
fix: deliver plugin agent session turns and replies (#10137)
## Thinking Path > - Paperclip manages agent execution through heartbeat runs and adapter-specific sessions > - Plugins can open an agent session and send a conversational message through the host service > - The host previously stored that message only in opaque wake payload metadata, so local adapters never saw it in their CLI prompt > - The host also forwarded run log chunks but did not expose the persisted final assistant text as the session reply > - This pull request defines both sides of the session contract in the shared wake renderer and terminal run event > - The benefit is that local adapters receive the actual conversational turn and plugins receive one canonical final reply ## Linked Issues or Issue Description Related context: Refs #629 and Refs #2880 describe adjacent `claude_local` final-text visibility failures. They concern issue comments rather than plugin agent sessions, but exercise the same need for a canonical persisted run summary. Companion consumer change: paperclipai/paperclip-gateway#3. Bug description: - **Observed:** calling the plugin host's `agents.sessions.sendMessage()` with `prompt: "hello"` woke a `claude_local` agent, but the generated CLI prompt omitted `hello`. On completion, the session emitted log chunks and a generic `Run completed` done event, so callers could not reliably recover the assistant reply. - **Expected:** the prompt becomes the user-supplied conversational turn for that agent session, and the successful terminal event carries the run's canonical final user-facing assistant text. - **Reproduction:** create a plugin agent session for a local adapter, call `sendMessage()` with a non-empty prompt, inspect the adapter prompt and terminal session event. - **Affected baseline:** `b517b887a` on `master`, local trusted deployment with plugin host services and `claude_local`; `codex_local` shared the wake-rendering gap because both use the common Paperclip wake prompt renderer. ## What Changed - Added a typed `agentMessage` wake payload rendered by the shared adapter prompt path used by `claude_local`, `codex_local`, and other local adapters. - Labeled session content as user-supplied and explicitly non-authoritative: it cannot expand authorization, permissions, task scope, or company boundaries. - Preserved ordinary heartbeat behavior by omitting the section when no agent-session message exists. - Added canonical `finalText` to terminal heartbeat status events from the already-persisted run summary/result/message. - Defined successful `AgentSessionEvent.message` as the canonical final user-facing reply (or `null`) and forwarded it on the terminal `done` event. - Added host, wake-renderer, normal-heartbeat, and terminal-reply regression coverage. ## Verification - `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts server/src/__tests__/heartbeat-agent-session-message.test.ts server/src/__tests__/heartbeat-run-status-payload.test.ts server/src/__tests__/plugin-agent-sessions.test.ts server/src/__tests__/heartbeat-run-summary.test.ts` — 87 passed. - `pnpm -r typecheck` — passed across all 31 workspaces. - `pnpm build` — passed. - `pnpm test:run` — 2,860 passed, 1 skipped, 3 unrelated failures: two existing macOS temp-path alias assertions (`/tmp` vs `/private/tmp`) in workspace branch-containment tests and one reproducible auto-port runtime-service adoption failure. The same three failures reproduce when the two files run alone; none touch this change. - Live Slack verification intentionally remains operator-gated because it requires rebuilding/restarting the host. ## Risks - User-controlled chat text now reaches the model prompt, which is an intentional prompt-injection surface. The renderer labels it as untrusted conversational content, while the existing plugin/session company checks and caller authorization remain unchanged. - `finalText` is added to company-scoped heartbeat status events. It is derived from the same persisted summary/result/message already used for run comments; no raw stdout or secrets are added. - Consumers that ignore the new field remain compatible, and successful runs without usable final text still emit `message: null`. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex (GPT-5), agentic reasoning with repository/tool use and code execution; context-window size is not surfaced in this environment. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> |
||
|
|
e2068319e7 |
fix(interactions): tolerate legacy stored result outcomes so listInteractions can't fail the whole list (#10119)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents and humans coordinate on issues through interaction requests
(confirmations, decisions, task suggestions and more) that are stored
per issue and listed by both the web UI and plugin workers such as chat
gateways
> - `listForIssue` hydrates every stored interaction row by hard-parsing
its persisted `result` blob against the current Zod schema
> - Stored rows outlive code: one live row written by an older build
carried `result.outcome: "withdrawn_by_creator"`, a value no longer in
the enum, and that single row made hydration throw
> - Because the throw happened inside the list mapping, it failed the
entire issue's interaction list — the web thread errored, and every
plugin consumer of `issues.listInteractions` (notification drain, digest
confirmation sweep, pending-ledger reads) failed continuously, so
interaction cards never reached chat surfaces
> - This pull request parses stored `result` blobs tolerantly — a
`parseStoredInteractionResult` helper wrapping `safeParse`, applied to
all five interaction kinds — so an unparseable result degrades to `null`
with a warning instead of failing the whole list
> - The benefit is durable robustness at the storage→hydrate boundary:
legacy or future schema drift in a single row can no longer take down an
issue's entire interaction surface
## Linked Issues or Issue Description
No pre-existing public issue; the underlying problem is described here
following the bug-report template. Related (not a duplicate): Refs #6709
— the creator-withdraw flow it explores matches the legacy outcome value
observed in the wild; whether or not that lineage wrote the row, this PR
is defensive against any such stored-schema drift.
**What happened**
Listing interactions for an issue (`GET /api/issues/:id/interactions` on
the web, or the `issues.listInteractions` plugin RPC) fails for the
entire issue when any single stored interaction row carries a
`result.outcome` written by an older build (observed live:
`"withdrawn_by_creator"`). Downstream plugin consumers that poll this
RPC fail continuously — notification drain, digest confirmation sweep,
and pending-ledger reads.
**Expected behavior**
One legacy/unreadable stored `result` should degrade gracefully — the
interaction still lists with its result treated as absent — rather than
failing the whole issue's interaction list.
**Steps to reproduce**
1. Persist a resolved `request_confirmation` interaction whose
`result.outcome` is not in the current enum (e.g.
`"withdrawn_by_creator"`, as written by an older build).
2. Call `issues.listInteractions` (or `GET
/api/issues/:id/interactions`) for that issue.
3. The call throws `invalid_enum_value` and returns nothing, instead of
returning the remaining rows.
**Version or commit**
master @
|
||
|
|
f2f168f6a1 |
fix(plugins): seed proactive company scopes before worker setup() + events.subscribe resolver parity (LOOA-695) (#10113)
- [x] I searched the GitHub PR list for similar PRs (dedup search). No open PR touches the proactive `events.subscribe` ordering path; #10103 (merged) is the predecessor whose ordering bug this fixes. ## Thinking Path The gateway worker's outbound push path is permanently dead (`eventSubscriptions: 0`, `notifier.received: 0`, `decisions.delivered: 0`). The plugin loader authorizes the worker's **proactive company scopes only AFTER `startWorker` resolves**, but a proactive plugin issues its one-shot `events.subscribe` calls from `setup()` — which runs *while `startWorker` is still awaiting the worker's initialize response*. So at subscribe time `proactiveCompanyScopes` is still empty → `contextForWorkerMessage` resolves no scope → the governed-access gate rejects every subscribe with `company context is required`. The gateway subscribes once and never retries, so `eventSubscriptions` stays 0 for the worker's life. This is an **ordering bug in the #10103 fix**, not a new method — same #9557 governed-access class as `config.get` (#10092) and `state.get` (#10103). Confirmed live at the 18:21:21Z worker respawn on `3093c5e` (host log), and again at the 19:01:04Z restart (still `events.subscribe: company context is required`, `eventSubscriptions:0`). ## What Changed 1. **Loader ordering** (`plugin-loader.ts`): load `registry.listConfigs(pluginId)` in a new step 4b **before** `startWorker`, and thread the configured company set into `WorkerStartOptions.proactiveCompanyScopes` so the worker handle is authorized *before the child process issues any host call*. The same rows are reused for startup config delivery (step 5b) — no second `listConfigs` round-trip. The runtime config-change path (`routes/plugins.ts`) still refreshes scopes via `setProactiveCompanyScopes` (unchanged). 2. **Handle seed** (`plugin-worker-manager.ts`): `createPluginWorkerHandle` seeds its `proactiveCompanyScopes` set from options at creation, before spawn. 3. **Resolver/gate parity** (`plugin-worker-manager.ts`): `referencedCompanyId(method, params)` now mirrors the SDK gate `requestedCompanyScope` exactly in the functional direction — adds `events.subscribe → params.filter.companyId` (how `ctx.events.on(name, { companyId }, fn)` issues its subscribe), and declines the gate's wildcard cases (`companies.list`, `scopeKind:"company"` without `scopeId`) so proactive access only ever grants a **single explicit configured company, never "all"**. Answers LOOA-693 AC#4 (host/gate extraction parity) in the functional direction. ## Tests New `plugin-worker-manager.test.ts` cases (drive a real worker): - a `setup()`-time `events.subscribe({ filter: { companyId } })` for an options-seeded company is **admitted** (fails on prior code — no options seed, no filter parity); - an unconfigured company stays **denied**; - an unseeded worker stays **denied**. Full `plugin-worker-manager.test.ts` suite: **21 passed**. Server `tsc --noEmit`: clean. All PR CI green (typecheck, server/workspace suites, e2e, build, security scans). ## Risks - **Scope-widening risk (primary).** The change grants proactive host access keyed off configured company rows. Mitigated by: the authorized set is exactly `registry.listConfigs(pluginId).map(companyId)`; wildcard cases (`companies.list`, company-scoped key without `scopeId`) resolve to `null`, never `{ kind: all }`; empty/whitespace ids dropped; an empty config set grants zero proactive access. This is the surface SecurityEngineer must sign off (see Security gate). - **In-invocation path unchanged.** Calls carrying a host-issued `paperclipInvocationId` keep the existing strict single-company match; the proactive branch only applies when there is no invocation id — so no regression to the enforced request path. - **Blast radius.** Loader step 4b is best-effort: a `listConfigs` failure logs and proceeds with an empty seed (fails closed — no push, not a crash), matching today's behavior. ## Model Used Claude Opus 4.8 (`claude-opus-4-8`) via Claude Code (agent: CTO). ## Security gate Touches the company-scope resolution path (same surface as #10103). Routed through **SecurityEngineer review before merge** (tracked on LOOA-696) — must not widen beyond configured companies; in-invocation strict single-company match untouched; wildcard cases deliberately declined in the proactive direction. ## Verification once live - Host log clean of `events.subscribe: company context is required` at worker start - loader logs `eventSubscriptions: N>0` - beat `notifier.received` / `decisions.delivered` move on real issue/approval activity Parent: LOOA-629 (outbound push half of "gateway active"). LOOA-695. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3093c5e694 |
fix(plugin-worker): resolve a company scope for proactive worker→host calls (#10103)
Host authorizes the plugin's configured companies as the worker's proactive scopes, set by the loader right after the #10092 config-delivery step and refreshed on operator config-save. At the single worker→host chokepoint, a no-invocation call (notifier drain, decision reconcile, mirror drain, digest, aging, liveness beat) that references a configured company resolves to that company's scope, so the #9557 governed-access gate admits it. One change covers the full proactive surface (state.*, issues.*, approvals.*, config.get, secrets.resolve, etc.). Safety: never widens beyond configured companies (any other company stays denied); in-invocation calls keep #9557's strict single-company match untouched. Fixes the Slack gateway DM round-trip for LOOA-629. Security review PASS (LOOA-693); non-blocking LOW follow-up tracked in LOOA-694. Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|
|
a7186dce4b |
fix(plugin-sdk): thread companyId through configChanged + fail-closed cross-tenant guard (LOOA-687) (#10096)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - First-party **plugins** run as isolated workers spawned by the host
`plugin-loader`, reading company-scoped config through a governed
`ctx.config.get(companyId)` channel.
> - The host→worker `configChanged` RPC carries `{ config, companyId }`,
but the SDK dispatch dropped the scope — `onConfigChanged(newConfig)`
was companyId-blind by design — so a **proactive** worker kept a single
worker-global config.
> - #10092 added a startup replay that fans out **every** stored
company's config through `configChanged`. With no deterministic
ordering, a plugin configured for more than one distinct company ends up
running as whichever DB row was delivered last.
> - That is a latent cross-tenant identity/secret confusion bug: one
company's bot token could be applied to another company's traffic.
> - This pull request threads `companyId` through `onConfigChanged` and
adds a fail-closed cross-tenant guard at the SDK layer, so a
single-tenant worker can never silently collapse to a second company's
config.
> - The benefit is that the config-delivery class is fixed at the SDK
boundary — before any genuinely multi-company proactive plugin ships —
without changing today's single-tenant behavior.
## Linked Issues or Issue Description
No public GitHub issue — describing in-PR (hardening / latent security):
**Latent cross-tenant config collapse.** The worker-side `configChanged`
dispatch forwarded only `config` and dropped `companyId`, so a proactive
plugin kept a single worker-global config. #10092's startup replay
delivers every configured company's config sequentially with no `ORDER
BY`, so a plugin with configs for more than one distinct company would
apply a nondeterministic last-write-wins global config (one tenant's
credential applied to another's traffic).
- Builds on and must merge after #10092.
- Not exploitable today: the only proactive consumer (the chat gateway)
has single-tenant config rows, so last-write-wins is a no-op. This is a
hardening pre-condition before any multi-company proactive plugin ships.
## What Changed
- **Thread scope through:** `onConfigChanged(newConfig, context)` with a
new exported `PluginConfigChangeContext { companyId }`. Backward
compatible — the second arg is optional; existing single-arg
implementations are unaffected.
- **Fail-closed cross-tenant guard** (`worker-rpc-host.ts`): a
single-tenant plugin that receives `configChanged` for a second,
distinct company with a *different* config is rejected with the new
`PLUGIN_RPC_ERROR_CODES.CROSS_TENANT_CONFIG` instead of silently
overwriting the applied tenant's config. Idempotent replays of the
*same* config under a different scope row remain allowed.
- **Opt-in `multiCompanyConfig: true`** on the plugin definition for
plugins that genuinely serve multiple companies from one worker (keying
per-company state on `context.companyId`); the guard is bypassed for
those.
- **Deterministic `ORDER BY companyId`** on `registry.listConfigs`, so
the startup replay binds a single-tenant worker to a stable company
across restarts.
- **Loader visibility:** a `CROSS_TENANT_CONFIG` rejection is logged at
`warn` (was best-effort `debug`) so the misconfiguration is surfaced.
- **Regression test**
(`packages/plugins/sdk/tests/worker-rpc-host.test.ts`): two distinct
companies delivered via the startup-replay path fail closed and stay
bound to the first company; an idempotent same-config replay under a
different scope row is allowed; a `multiCompanyConfig` plugin receives
per-company config with the correct `context.companyId`.
## Verification
- SDK `tsc --noEmit`: clean.
- SDK vitest `worker-rpc-host.test.ts`: 7/7 pass (incl. 3 new). The
two-distinct-company case **fails against pre-fix code** and passes
after the fix.
- #10092 embedded-postgres `plugin-config-startup-delivery.test.ts`: 3/3
pass (unaffected by the new `ORDER BY`).
- Full server `tsc --noEmit` against this SDK: clean.
## Risks
- **Low functional risk.** The second `onConfigChanged` arg is optional
and existing implementations are unchanged. Today's single-tenant
gateway keeps working — idempotent same-config replays are explicitly
allowed, so the go-live is preserved.
- **Behavioral shift on misconfig:** a genuinely multi-company plugin
that has NOT opted into `multiCompanyConfig` now fails closed
(`CROSS_TENANT_CONFIG`) rather than silently collapsing to one tenant.
This is the intended safer default; opt in with `multiCompanyConfig:
true` to serve multiple companies from one worker.
- **Not in scope (residual).** Per-company workers/connections for a
genuinely multi-company gateway increase resource use and are tracked
separately (ties into the #10092 fan-out/timeout follow-up). This PR
fixes the class and fails closed; it does not build multi-tenant
connection management.
## Model Used
Claude — Anthropic `claude-opus-4-8` (Opus 4.8), extended thinking, with
tool use / code execution via Claude Code.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes:` / `Closes`
/ `Refs` OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change and contains no internal
Paperclip ticket id — branch predates this rule; not renaming an open PR
mid-review
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes — no
doc surface; internal SDK/host behavior only
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: anicca <annica@Michaels-Mac-Studio.local>
Co-authored-by: Paperclip <noreply@paperclip.ing>
|
||
|
|
1ebf5254b6 |
fix(plugin-loader): deliver stored config to freshly-started plugin workers (#10092)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - One capability is first-party **plugins** that run as isolated workers spawned by the host `plugin-loader`, reading company-scoped config through a governed `ctx.config.get(companyId)` channel. > - A **proactive** plugin (e.g. a chat gateway that opens a Slack Socket Mode connection at startup) does its company work from `setup()`, where there is **no company-scoped invocation** — so `ctx.config.get()` is rejected with `company context is required`. > - The worker swallows that error and falls back to its default (feature-off) config, so the plugin comes up **inert** even though correct config exists in the database. > - This is a regression from #9557 ("governed access contracts"), which changed `plugin-loader.ts` `activatePlugin` from loading stored config into the worker bootstrap to `const config = {}`. > - This pull request replays each configured company's stored config to the freshly-started worker over the **same `configChanged` host→worker path an operator config-save already uses**. > - The benefit is that proactive plugins receive their config on worker start (both server boot and operator enable) without weakening the governed-access surface. ## Linked Issues or Issue Description No public GitHub issue — describing in-PR (bug): **Bug.** After a proactive plugin's worker spawns, it never receives its stored config. Governed access (`packages/plugins/sdk/src/host-client-factory.ts`) only resolves `config.get` inside a company-scoped invocation (event/action/tool, or explicit `params.companyId`). Proactive plugins operate from `setup()` where no such scope exists, so `config.get()` fails with `company context is required`, the worker falls back to defaults, and the feature stays disabled despite valid DB config. - Regression introduced by #9557. - Related follow-up (latent multi-company hardening): #10096. ## What Changed - `plugin-registry.ts`: add read-only `listConfigs(pluginId)` returning all stored company config rows for a plugin (scoped `where eq(pluginConfig.pluginId, pluginId)`). - `plugin-loader.ts`: after the worker starts in `activatePlugin`, replay each company's stored config through the existing `configChanged` host→worker RPC — one `{ config, companyId }` per row, the same payload shape as the operator config-save path in `routes/plugins.ts`. Best-effort and idempotent; covers both server-boot `loadAll` and operator enable. - test: DB-backed `plugin-config-startup-delivery.test.ts` covering `registry.listConfigs` completeness and cross-plugin isolation. ## Verification - `tsc --noEmit` on `@paperclipai/server` — clean. - New `plugin-config-startup-delivery.test.ts` (embedded-postgres, 3 cases) — pass. - Full PR CI green: typecheck, all server/e2e/serialized test shards, build, canary dry-run, verify, and the security scanners (Snyk, Socket, Superagent, Greptile). ## Risks - **Low functional risk.** Adds an outbound host→worker push that mirrors the already-shipped operator-save path. A worker without an `onConfigChanged` handler (or momentarily unavailable) simply keeps the runtime `ctx.config.get(companyId)` model. - **Startup fan-out.** One `configChanged` per configured company at activation (sequential, default RPC timeout). `plugin_config` rows are writable only by instance-admins, so fan-out size is operator-controlled — not a remote surface. - **No secret-handling change.** `configJson` is delivered as-is, exactly as `config.get`/operator-save already deliver it. No new secret sink; catch-blocks log only ids + `err.message` at debug, never `configJson`. - **Latent multi-company behavior (pre-existing, not introduced here).** The worker-side `configChanged` dispatch forwards only `config` (drops `companyId`), and `listConfigs` has no `ORDER BY`, so a plugin configured for **more than one** company would apply a nondeterministic last-write-wins global config. This is existing SDK behavior — operator-save already pushes into the same handler — and is **not reachable by the single-company consumer this fix targets**. Greptile flagged this shape (4/5). It is tracked and fixed as a separate, non-blocking hardening PR (#10096): thread `companyId` through `onConfigChanged`, deterministic ordering, bounded fan-out. ## Model Used Claude — Anthropic `claude-opus-4-8` (Opus 4.8), extended thinking, with tool use / code execution via Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes:` / `Closes` / `Refs` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [ ] My branch name describes the change and contains no internal Paperclip ticket id — branch predates this rule; not renaming an open PR mid-review - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] I have updated relevant documentation to reflect my changes — no doc surface; internal SDK/host behavior only - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups — 4/5; two latent multi-company items triaged as non-blocking and fixed in follow-up #10096 (see Risks) - [x] I will address all Greptile and reviewer comments before requesting merge — addressed: triaged as non-blocking follow-up in #10096 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|
|
5a00040c4c |
feat(plugin-sdk): interactions/approvals respond + attachment read capabilities for the chat gateway (#10066)
Adds the remaining 5 plugin capabilities + 7 worker→host RPC methods (interactions read/respond, approvals read/respond, attachment read) needed by the Slack chat gateway plugin (v0.5.0) to pass manifest capability validation and load. - Security review: PASS (LOOA-642) after the viewer-role privilege-escalation blocker (LOOA-648) was fixed on this branch (requireActiveHumanMember now rejects viewer on impersonation write-paths, matching assertCompanyAccess). - CI: Build, Typecheck, all server suites (3/3 + serialized 4/4), workspaces, e2e shard 2/2, and all security scanners (Snyk/Socket/Superagent/Greptile/security-review) green. - One e2e flake (signoff-policy 'non-participant cannot advance stage') is unrelated: it exercises execution-policy stage advancement (routes/issues.ts, untouched by this PR) and failed on a heartbeat_run_events FK race + 409 checkout conflict. Unblocks LOOA-629 (Slack gateway go-live) and the interview-ask feature. Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|
|
e55d702916 |
feat(plugin-sdk): human-attributed issue comments for chat gateway plugins (#10050)
Adds the `issue.comments.create_human_attributed` capability and `ctx.issues.createComment` `actorUserId` option, with host-side active-human-member verification. LOOA-627. Co-Authored-By: Paperclip <noreply@paperclip.ing> |
||
|
|
1b8738da5c |
feat(cli): add --force to company export for non-interactive runs (#10054)
## Thinking Path
- `company export` writes into a target directory but aborts
non-interactively when that directory is non-empty ("already contains
files. Re-run interactively or choose an empty directory"), and there is
no override flag. Any automated caller that exports into a pre-existing
directory (for example a git clone used as a backup target) is therefore
stuck.
- The interactive confirmation is the right default for humans, but
automation needs an explicit opt-out rather than being forced to export
into a throwaway empty directory and copy the tree afterward.
- A `--force` flag that skips only the non-empty-directory confirmation
is the minimal change: it does not alter what gets written (existing
files are overwritten in place, nothing is bulk-deleted), so callers
keep full control over cleanup via their own VCS.
## What Changed
- Added a `--force` option to `company export` that skips the non-empty
output-directory confirmation for non-interactive/automated runs.
- Threaded the flag into `confirmOverwriteExportDirectory(outDir, {
force })`; behavior is unchanged when the flag is absent.
- Updated the non-interactive error message to also mention `--force`.
- Added focused unit tests covering: missing dir (resolves), empty dir
(resolves), non-empty dir without force (throws), non-empty dir with
force (resolves), and a path that exists but is a file (throws).
## Verification
- `pnpm --filter @paperclipai/cli exec vitest run
src/__tests__/company-export-force.test.ts` → 5/5 pass.
- `tsc --noEmit` over the CLI sources: no new type errors (the only
errors are pre-existing `@paperclipai/plugin-sdk` module-not-found in
`server/` from an unbuilt plugin sdk in the sandbox, unrelated to this
change).
- End-to-end against a local server: exporting into a non-empty
directory fails without `--force` and succeeds with it; a `.git`
directory and a sentinel file in the target were preserved; 128 files
written.
## Risks
- Low. The flag is opt-in and defaults to false; interactive and
empty-directory behavior is untouched. `--force` overwrites matching
files in place but never deletes unrelated files, so it cannot silently
wipe a directory.
## Model Used
Claude Opus 4.8 (claude-opus-4-8)
---
**Problem or motivation**
`company export` aborts when the `--out` directory is non-empty and
stdin/stdout are not a TTY, and there is no override flag. This makes it
impossible to run `company export` unattended into a pre-existing
directory such as a git clone.
**Proposed solution**
Add a `--force` flag that skips the non-empty-directory confirmation for
non-interactive callers. Files are still written on top of existing
content with no bulk delete, so unrelated files such as `.git` are
preserved.
**Alternatives considered**
Exporting into a fresh temp directory and copying the tree into the real
target afterward works but is clumsy and error-prone for automation;
broadening or removing the guard entirely would remove a useful safety
net for interactive users.
**Roadmap alignment**
Hardens the automated/unattended export path used by scheduled
company-backup routines.
---
- [x] I searched the repository and open pull requests for similar or
duplicate PRs and found none.
Co-authored-by: anicca <annica@MichaelacStudio.localdomain>
Co-authored-by: Paperclip <noreply@paperclip.ing>
|