diff --git a/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.test.ts b/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.test.ts index 6bb6141040f..d3170754700 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.test.ts @@ -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(); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.ts b/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.ts index bb2f8249bce..2cea9e88e04 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-company-secret-confirmation.ts @@ -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|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]}};