mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 01:50:13 +01:00
fix(connections): keep agent access consistent through setup and permission changes (#19352)
Task-hosted tool setup kept requester-only defaults, and audience edits could leave runtime installations restricted after permission bindings were widened. Large provider catalogs could also fail before creating an access card. Use the normal agent audience defaults for new tool setup, preserve explicit selections through verified OAuth and draft resumption, and synchronize permission bindings and runtime installations atomically. Enforce agent-edit authority and preserve existing access during additive account reuse. Validation: 915 focused regression tests, typechecks, and builds passed. All 54 CI checks passed and Greptile Apex scored the reviewed head 5/5 with no unresolved threads. Broad local test failures, recoveries, and incomplete heartbeat coverage are documented in the PR. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
36643d9378
commit
8f79a32898
27 files changed
+489
-91
No files matched your search
@@ -57,12 +57,24 @@ describe("connection intent contracts", () => {
|
||||
requestingAgentName: "CEO", phase: "requested" };
|
||||
const payload = (tools: unknown[]) => ({ ...base, accessRequest: { connectionId: agentId, connectionName: "Account", tools } });
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload([tool])).success).toBe(true);
|
||||
const catalog = Array.from({ length: 46 }, (_, i) => ({ ...tool,
|
||||
catalogEntryId: `11111111-1111-4111-8111-${String(i).padStart(12, "0")}`, toolName: `tool_${i}` }));
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload(catalog)).success).toBe(true);
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload([])).success).toBe(false);
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload([tool, tool])).success).toBe(false);
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload([{ ...tool, versionHash: "" }])).success).toBe(false);
|
||||
expect(connectionIntentPayloadSchema.safeParse(payload([{ ...tool, permission: "allow_all" }])).success).toBe(false);
|
||||
});
|
||||
|
||||
it("binds a saved OAuth audience to a connection without accepting credentials", () => {
|
||||
const base = { version: 1, serviceSlug: "github", serviceName: "GitHub", requestingAgentId: agentId,
|
||||
requestingAgentName: "Onboarding", phase: "authorizing" };
|
||||
const setupAccess = { connectionId: agentId, access: "all_agents" };
|
||||
expect(connectionIntentPayloadSchema.parse({ ...base, setupAccess }).setupAccess).toEqual(setupAccess);
|
||||
expect(connectionIntentPayloadSchema.safeParse({ ...base, setupAccess: { ...setupAccess, token: "secret" } }).success).toBe(false);
|
||||
expect(connectionIntentPayloadSchema.safeParse({ ...base, setupAccess: { access: "all_agents" } }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps generic interaction creation closed to the server-owned kind", () => {
|
||||
expect(createIssueThreadInteractionSchema.safeParse({
|
||||
kind: "connection_intent",
|
||||
|
||||
@@ -1197,6 +1197,10 @@ const connectionIntentBrandAssetSchema = z
|
||||
|
||||
export const connectionIntentPayloadSchema = z
|
||||
.object({
|
||||
setupAccess: z.object({
|
||||
connectionId: z.string().guid(),
|
||||
access: z.union([z.literal("all_agents"), z.object({ agentIds: z.array(z.string().guid()).max(250) }).strict()]),
|
||||
}).strict().optional(),
|
||||
savedSetup: z.object({ endpointId: z.string().guid(), connectionId: z.string().guid(), action: z.enum(["enter_credentials", "rotate_credentials", "confirm_identity"]).optional(), credentialRotation: z.object({ previousId: z.string().guid().nullable() }).strict().optional(), credentialEntry: z.object({
|
||||
previousGenerations: z.record(z.string().refine(field => ["signingSecret", "webhookSecret", "privateKey", "clientSecret", "projectSecret"].includes(field)), z.string().uuid().nullable()).refine(value => Object.keys(value).length > 0 && Object.keys(value).length <= 5),
|
||||
requireAll: z.boolean(),
|
||||
@@ -1210,7 +1214,7 @@ export const connectionIntentPayloadSchema = z
|
||||
toolName: z.string().trim().min(1).max(160),
|
||||
versionHash: z.string().min(1).max(256),
|
||||
permission: z.enum(["allowed", "ask_first"]),
|
||||
}).strict()).min(1).max(20).refine(tools => new Set(tools.map(tool => tool.catalogEntryId)).size === tools.length, "Requested tools must be unique"),
|
||||
}).strict()).min(1).max(500).refine(tools => new Set(tools.map(tool => tool.catalogEntryId)).size === tools.length, "Requested tools must be unique"),
|
||||
}).strict().optional(),
|
||||
upstreamService: z.object({ slug: z.string().min(1).max(120), name: z.string().min(1).max(160), selectionInteractionId: z.string().guid().optional() }).strict().optional(),
|
||||
purpose: z.enum(["ai", "channel"]).optional(),
|
||||
|
||||
Reference in new issue
Block a user