mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
Recognize ordered secure-save acknowledgments across explanatory clauses
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
5f74dc6545
commit
bf22f7ba69
2 files changed
+43
-2
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import {expect,it} from "vitest";
|
||||
import {readFile} from "node:fs/promises";
|
||||
import {canonicalCompanySecretKey,companySecretConfirmationHandoff,validCompanySecretMetadata} from "./external-company-secret-confirmation.js";
|
||||
import {canonicalCompanySecretKey,companySecretConfirmationHandoff,companySecretSaveMatches,validCompanySecretMetadata} from "./external-company-secret-confirmation.js";
|
||||
import {deploymentSecretGuidance,companyOperatorBindingRequested} from "./external-company-secret.js";
|
||||
const prompt='I cannot store the deployment key value for you: only a human board operator can enter it. In Settings > Secrets (`/RUN/company/settings/secrets`), choose New secret, name it "Deployment API Key", set key `DEPLOYMENT_API_KEY`, save the value, then accept this card. Do not paste the key here. I will bind the saved secret to Morgan\'s environment.';
|
||||
const binding={companyId:"company",issueId:"source",agentId:"ceo",userId:"person",runId:"first",canonicalCommentId:"human"};
|
||||
@@ -42,3 +42,30 @@ it("admits a bound completed-entry question with native Confirm and rejects othe
|
||||
expect(companySecretConfirmationHandoff({...card(),payload:{...card().payload,prompt}},binding,guidance),prompt).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("admits explicit secure-save acknowledgments across explanatory sentences and paragraphs",()=>{
|
||||
const actual='I can\'t store credential values from chat. Please open /RUN/company/settings/secrets, choose New secret, and save the deployment key there. Suggested name: "Deployment API key"; suggested key/env var: DEPLOYMENT_API_KEY. Then confirm here that secure entry is complete so I can bind it to Morgan\'s environment. Do not paste the key in chat or in this card.';
|
||||
const paragraph="Open Settings > Secrets (`/RUN/company/settings/secrets`) and save the deployment key there.\n\nUse the company-owned secret entry form.\n\nThen accept this card to confirm the deployment key is saved so I can bind its reference to Morgan's environment. Never paste the value here.";
|
||||
for(const text of [actual,paragraph])expect(companySecretConfirmationHandoff({...card(),payload:{...card().payload,prompt:text}},binding,guidance),text).toBeDefined();
|
||||
const context="Use Settings > Secrets (`/RUN/company/settings/secrets`) for the deployment key; I will bind the saved reference to Morgan's environment. ";
|
||||
for(const text of [
|
||||
"May I save the deployment key? Then accept this card.",
|
||||
"Confirm this card first. Save the deployment key there.",
|
||||
"Do not save the deployment key. Then accept this card.",
|
||||
"Save the deployment key, then cancel the save. Then accept this card.",
|
||||
"Save your profile. Then confirm here that secure entry is complete.",
|
||||
"Save the other key. Then accept this card.",
|
||||
"Earlier, save the deployment key there. Then accept this card.",
|
||||
"Save the deployment key there. Then confirm the other card.",
|
||||
"Save the deployment key there. Then confirm permission to bind Morgan's secret reference.",
|
||||
"Save the deployment key there. Then accept this card and paste the key value here.",
|
||||
])expect(companySecretConfirmationHandoff({...card(),payload:{...card().payload,prompt:context+text}},binding,guidance),text).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps secure-save acknowledgment acceptance dependent on a later current-person save",()=>{
|
||||
const handoff=companySecretConfirmationHandoff(card(),binding,guidance)!;
|
||||
const saved={id:"secret",companyId:"company",scope:"company",status:"active",createdByUserId:"person",createdAt:"2026-11-04T13:30:00Z",name:"Deployment API Key",key:"deployment_api_key"};
|
||||
expect(companySecretSaveMatches(saved,binding,handoff)).toBe(true);
|
||||
for(const change of [{createdAt:"2026-11-04T13:28:00Z"},{createdByUserId:"other"},{companyId:"other"},{key:"other_key"},{name:"Other secret"}])expect(companySecretSaveMatches({...saved,...change},binding,handoff)).toBe(false);
|
||||
expect(companySecretConfirmationHandoff({...card(),originCommentIds:["other"]},binding,guidance)).toBeUndefined();
|
||||
});
|
||||
@@ -12,6 +12,20 @@ export function validCompanySecretMetadata(value:unknown):value is {name?:string
|
||||
return Object.entries(metadata).every(([key,v])=>v===undefined||typeof v==="string"&&v.length>0&&v.length<=120&&v.trim()===v&&!/[\r\n=]|https?:\/\//i.test(v)&&!/(?:^|\s)(?:sk|pk|gh[pousr]|github_pat|xox[baprs])[-_]|[A-Za-z0-9]{32}/i.test(v)&&(key!=="key"||/^[a-zA-Z0-9_.-]+$/.test(v)));
|
||||
}
|
||||
export interface CompanyConfirmationBinding extends SecretQuestionBinding {canonicalCommentId:string}
|
||||
function orderedSecureSaveAcknowledgment(prompt:string){
|
||||
const clauses=[...prompt.matchAll(/[^.!?;\n]+/g)];
|
||||
for(const clause of clauses){
|
||||
const save=/\b(?:save|enter|set)\s+(?:the\s+)?(?:deployment\s+(?:api\s+)?(?:key|credential|secret)|(?:secret|credential|key)\s+value|value|it)\b/i.exec(clause[0]);
|
||||
if(!save||/\b(?:not|never|don.t|cancel|decline|skip|avoid|earlier|previously|formerly|may I|can I|will you)\b/i.test(clause[0].slice(0,save.index)))continue;
|
||||
const after=prompt.slice(clause.index!+save.index+save[0].length);
|
||||
for(const acknowledgment of after.matchAll(/\b(?:accept|confirm)\b[^.!?;\n]*/gi)){
|
||||
const between=after.slice(0,acknowledgment.index),intent=acknowledgment[0];
|
||||
if(/\b(?:cancel|decline|skip|avoid|do not|don.t|never)\b/i.test(between)||/\b(?:before|permission|authorize|another|other card)\b/i.test(intent))continue;
|
||||
if(/^(?:accept|confirm)\s+(?:this\s+card|here\b[^.!?;\n]{0,80}\b(?:secure entry|saved|complete)|(?:that\s+)?(?:the\s+)?(?:deployment\s+(?:api\s+)?key|secure entry)\b[^.!?;\n]{0,80}\b(?:saved|complete))/i.test(intent))return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function companySecretConfirmationHandoff(value:unknown,b:CompanyConfirmationBinding,guidance:(text:string)=>boolean,status:"pending"|"accepted"="pending"){
|
||||
const row=value as Partial<RequestConfirmationInteraction>|undefined,p=row?.payload;
|
||||
if(!row?.id||row.kind!=="request_confirmation"||row.status!==status||row.companyId!==b.companyId||row.issueId!==b.issueId||row.sourceRunId!==b.runId||row.createdByAgentId!==b.agentId||row.createdByUserId||row.addresseeAgentId||(row.addresseeUserId!=null&&row.addresseeUserId!==b.userId)||!row.originCommentIds?.includes(b.canonicalCommentId)||!chatCatalogHumanQuestionPolicies(row)||(row.continuationPolicy!=="wake_assignee"&&row.continuationPolicy!=="wake_assignee_on_accept")||row.dismissedAt||!p||!requestConfirmationPayloadSchema.safeParse(p).success||p.target||p.toolAction||p.secretProposal||p.connectionAuthorization||p.qualityReview
|
||||
@@ -19,7 +33,7 @@ export function companySecretConfirmationHandoff(value:unknown,b:CompanyConfirma
|
||||
const text=[row.title,p.prompt,p.detailsMarkdown,p.acceptLabel,p.rejectLabel].filter(Boolean).join("\n");
|
||||
const completedQuestion=p.prompt.match(/\bhave\s+you\s+(?:already\s+)?(?:saved|set|entered|completed)\b[^?!\n]{0,240}\?/i)?.[0];
|
||||
const completedEntry=!!completedQuestion&&/\bdeployment\s+(?:api\s+)?(?:key|credential|secret)\b/i.test(completedQuestion);
|
||||
if(!guidance(text)||(!/\b(?:save|saved|set|entered)\b[^.!?\n]{0,100}\b(?:accept|confirm)\b/i.test(p.prompt)&&!completedEntry)||!/^(?:Confirm|Accept|Done|Saved)$/i.test(p.acceptLabel??""))return;
|
||||
if(!guidance(text)||(!orderedSecureSaveAcknowledgment(p.prompt)&&!completedEntry)||!/^(?:Confirm|Accept|Done|Saved)$/i.test(p.acceptLabel??""))return;
|
||||
const names=[...p.prompt.matchAll(/\bname\s+(?:it|the secret)\s+["“]([^"”\n]{1,120})["”]/gi)],keys=[...p.prompt.matchAll(/\b(?:set\s+)?(?:secret\s+)?key\s+`([a-zA-Z0-9_.-]{1,120})`/gi)];
|
||||
if(names.length>1||keys.length>1||/\bname\s+(?:it|the secret)\b/i.test(p.prompt)&&names.length!==1||/\bset\s+(?:the\s+)?key\b/i.test(p.prompt)&&keys.length!==1||!validCompanySecretMetadata({name:names[0]?.[1],key:keys[0]?.[1]}))return;
|
||||
return {interactionId:row.id,continuationPolicy:row.continuationPolicy,createdAt:String(row.createdAt),payloadHash:createHash("sha256").update(JSON.stringify(p)).digest("hex"),acceptLabel:p.acceptLabel!,prompt:p.prompt,metadata:{name:names[0]?.[1],key:keys[0]?.[1]}};
|
||||
|
||||
Reference in new issue
Block a user