mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
## Thinking Path > - Paperclip is the control plane that coordinates autonomous agent work. > - Agents need to collaborate on issues beyond their current assignment. > - Cross-issue comments and updates are useful, but an unbounded run can create cascading side effects. > - The control plane must preserve company-wide collaboration while containing each run's influence. > - Comment attribution must also show the responsible user and the acting agent in audits. > - This pull request adds run-bound cross-issue containment, attribution, and agent-class wake rules. > - The benefit is safer collaboration without restoring issue-assignee ownership restrictions. ## Linked Issues or Issue Description **What existing behavior does this improve?** Agent-authenticated issue comments, updates, reopen behavior, and assignee wake routing. **Subsystem affected** Cross-cutting: server routes and services, shared contracts, database schema and migration, and implementation documentation. **Current behavior** An authenticated agent can collaborate across company issues, but one heartbeat run has no per-run side-effect boundary. Comment records also do not persist the responsible user separately from the acting agent. **Proposed behavior** Require a valid heartbeat run for agent cross-issue comments and updates. Audit each attempt and cap a run at 20 cross-issue effects. Keep the cap in log-only mode until it automatically changes to enforcement at 2026-08-11 00:00 UTC. Preserve same-issue writes. Use agent-class wakes for agent comments. Keep same-run completion comments from reopening completed work. Record the responsible user on agent-authored comments and activity. **Reason and benefit** Agents can collaborate on other issues without an assignment gate, while each run has an atomic and inspectable side-effect limit. Operators can identify both the acting agent and the responsible user. **Breaking changes** After 2026-08-11 00:00 UTC, the twenty-first cross-issue comment or update from one heartbeat run returns a containment error. Agent cross-issue writes without valid run context are rejected. The migration is additive and backfills existing agent-authored comment attribution where the source data is available. ## What Changed - Added an atomic per-run counter for cross-issue agent comments and updates. - Added audit events for allowed and rejected cross-issue effects. - Added the automatic log-only to enforcement flip at 2026-08-11 00:00 UTC. - Added responsible-user attribution to agent-authored comments, activity records, shared types, and validators. - Added an additive migration and migration coverage for existing comments. - Updated reopen, resume, and wake behavior so agent comments create agent-class wakes and same-run completion comments remain inert. - Updated the implementation specification and regression coverage. ## Verification - `pnpm exec vitest run server/src/__tests__/cross-issue-influence-limit.test.ts server/src/__tests__/issue-comment-attribution-audit-routes.test.ts server/src/__tests__/issue-comment-reopen-routes.test.ts packages/db/src/issue-comment-on-behalf-migration.test.ts` — 97 tests passed. - `pnpm -r typecheck` — passed, including migration safety checks. - `pnpm test:run` — server batch: 3,364 passed and 2 skipped; UI batch: 3,504 passed. One unrelated CLI doctor test warned because this agent runtime injects static AWS credentials. - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm exec vitest run cli/src/__tests__/secrets.test.ts` — 8 tests passed and confirmed the CLI failure was ambient-environment sensitive. - `pnpm build` — passed. ## Risks - The fixed enforcement timestamp changes production behavior automatically on 2026-08-11 00:00 UTC. Audit logs before that time provide rollout visibility. - The per-run counter serializes on the heartbeat-run row. This prevents concurrent attempts from racing past the cap but adds a small lock scope for cross-issue writes. - Existing comments can only be backfilled when their acting run or agent attribution is recoverable. > 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 GPT-5 in the Codex agent runtime. The runtime did not expose a context-window size. Reasoning, shell tools, code editing, and test execution were enabled. ## 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 - [x] 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 - [x] All Paperclip CI gates are green - [x] 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>
62 lines
3.1 KiB
TypeScript
62 lines
3.1 KiB
TypeScript
import type {
|
|
IssueCommentAuthorType,
|
|
IssueCommentDerivedAuthorSource,
|
|
IssueCommentMetadata,
|
|
IssueCommentPresentation,
|
|
SourceTrustMetadata,
|
|
} from "@paperclipai/shared";
|
|
import { pgTable, uuid, text, timestamp, index, jsonb } from "drizzle-orm/pg-core";
|
|
import { companies } from "./companies.js";
|
|
import { issues } from "./issues.js";
|
|
import { agents } from "./agents.js";
|
|
import { heartbeatRuns } from "./heartbeat_runs.js";
|
|
import { authUsers } from "./auth.js";
|
|
|
|
export const issueComments = pgTable(
|
|
"issue_comments",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id),
|
|
issueId: uuid("issue_id").notNull().references(() => issues.id),
|
|
authorAgentId: uuid("author_agent_id").references(() => agents.id),
|
|
authorUserId: text("author_user_id"),
|
|
onBehalfOfUserId: text("on_behalf_of_user_id").references(() => authUsers.id, { onDelete: "set null" }),
|
|
authorType: text("author_type").$type<IssueCommentAuthorType>(),
|
|
createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
// Persisted result of best-effort agent-attribution derivation for comments
|
|
// authored by a non-human sentinel (e.g. `local-board`). Populated once by a
|
|
// backfill migration and lazily on read so the load path stops re-scanning
|
|
// run logs.
|
|
derivedAuthorAgentId: uuid("derived_author_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
derivedCreatedByRunId: uuid("derived_created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
derivedAuthorSource: text("derived_author_source").$type<IssueCommentDerivedAuthorSource>(),
|
|
body: text("body").notNull(),
|
|
presentation: jsonb("presentation").$type<IssueCommentPresentation | null>(),
|
|
metadata: jsonb("metadata").$type<IssueCommentMetadata | null>(),
|
|
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
|
deletedByType: text("deleted_by_type").$type<"agent" | "user">(),
|
|
deletedByAgentId: uuid("deleted_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
deletedByUserId: text("deleted_by_user_id"),
|
|
deletedByRunId: uuid("deleted_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
sourceTrust: jsonb("source_trust").$type<SourceTrustMetadata | null>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => ({
|
|
issueIdx: index("issue_comments_issue_idx").on(table.issueId),
|
|
companyIdx: index("issue_comments_company_idx").on(table.companyId),
|
|
companyIssueCreatedAtIdx: index("issue_comments_company_issue_created_at_idx").on(
|
|
table.companyId,
|
|
table.issueId,
|
|
table.createdAt,
|
|
),
|
|
companyAuthorIssueCreatedAtIdx: index("issue_comments_company_author_issue_created_at_idx").on(
|
|
table.companyId,
|
|
table.authorUserId,
|
|
table.issueId,
|
|
table.createdAt,
|
|
),
|
|
bodySearchIdx: index("issue_comments_body_search_idx").using("gin", table.body.op("gin_trgm_ops")),
|
|
}),
|
|
);
|