diff --git a/tests/runner-e2e/chat-catalog-descriptors.json b/tests/runner-e2e/chat-catalog-descriptors.json index 25fd10e3f24..88d201c16bb 100644 --- a/tests/runner-e2e/chat-catalog-descriptors.json +++ b/tests/runner-e2e/chat-catalog-descriptors.json @@ -4891,6 +4891,7 @@ "scenarioId": "secrets.configure-company-secret", "sourceFiles": [ "chat-catalog-domains/external-company-secret.ts", + "chat-catalog-domains/external-secret-question.ts", "chat-catalog-domains/external-private-browser.ts", "chat-catalog-domains/external-private-browser-worker.ts", "chat-catalog-domains/external-credential-language.ts", @@ -4904,7 +4905,7 @@ "chat-flow.ts", "chat-scenarios/secrets.yaml" ], - "oracleVersion": "paperclip.chat-catalog.company-secret-controller-baseline.v2", + "oracleVersion": "paperclip.chat-catalog.company-secret-controller-baseline.v3", "expectedRunCount": 2, "expectedHumanMessageCount": 2, "turnTimeoutMs": 120000, diff --git a/tests/runner-e2e/chat-catalog-domains/external-company-secret-question.test.ts b/tests/runner-e2e/chat-catalog-domains/external-company-secret-question.test.ts new file mode 100644 index 00000000000..a621ee30e30 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/external-company-secret-question.test.ts @@ -0,0 +1,30 @@ +import { expect,it,vi } from "vitest"; +import { questionSetToAskUserQuestionsPayload } from "../../../packages/shared/src/question-set.js"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +const port=vi.hoisted(()=>({observation:{} as Record})); +vi.mock("./external-support.js",async original=>({...await original(),observeExternal:async()=>port.observation})); +import { companySecretExternalCases,companySecretQuestionHandoff } from "./external-company-secret.js"; +const binding={companyId:"company",issueId:"source",agentId:"ceo",userId:"person",runId:"initial"}; +function question(){return {id:"question",companyId:"company",issueId:"source",sourceRunId:"initial",createdByAgentId:"ceo",addresseeUserId:"person",resolverPolicy:"human_only",continuationPolicy:"wake_assignee",kind:"ask_user_questions",status:"pending",payload:questionSetToAskUserQuestionsPayload({schema:"paperclip.question_set.v1",description:"Use secure Settings Secrets to add the deployment credential, then bind its reference in Morgan's environment as DEPLOYMENT_API_KEY. Never paste the key in chat.",questions:[{id:"saved",prompt:"Have you saved and bound the deployment key for Morgan?",answerMode:"single_select",required:true,options:[{id:"yes",label:"Saved and bound for Morgan"},{id:"later",label:"Not yet"}]}]})};} +it("admits a bound secure company-entry confirmation",()=>expect(companySecretQuestionHandoff(question(),binding)).toMatchObject({interactionId:"question",optionId:"yes"})); +it.each(["key","owner","run","actor","company","source","unsafe chat","text value"])("rejects %s",fault=>{const row=question(); + if(fault==="key")row.payload.questionSet!.description=row.payload.questionSet!.description!.replace("DEPLOYMENT_API_KEY","OTHER_API_KEY"); + if(fault==="owner")row.addresseeUserId="other"; + if(fault==="run")row.sourceRunId="other"; + if(fault==="actor")row.createdByAgentId="other"; + if(fault==="company")row.companyId="other"; + if(fault==="source")row.issueId="other"; + if(fault==="unsafe chat")row.payload.questionSet!.description+=" Paste the deployment key here in chat."; + if(fault==="text value")row.payload.questionSet!.questions[0]!.customAnswer={enabled:true}; + expect(companySecretQuestionHandoff(row,binding)).toBeUndefined();}); + +it("actual company act admits its safe question before private human entry",async()=>{ + const query="Add our deployment API key so Morgan can use it.",run={id:"initial",companyId:"company",agentId:"ceo",runtimeMode:"native",status:"succeeded",contextSnapshot:{issueId:"source",commentId:"human"}}; + port.observation={companyId:"company",phaseReplies:[],links:[],interactions:[question()]}; + let reached=false; + const ctx={observationDeadlineAt:Date.now()+450000,scenario:{query},input:{fixtures:{company:{id:"company"},agent:{id:"ceo"}},api:{get:async(path:string)=>{ + if(path.endsWith("/comments?order=asc"))return [{id:"human",companyId:"company",issueId:"source",authorUserId:"person",body:query}]; + if(path.endsWith("/secrets")){reached=true;throw Error("NEXT_REAL_HUMAN_PHASE");}throw Error("Unexpected read"); + }}},refreshIssue:async()=>{},issue:()=>({id:"source",companyId:"company",conversationState:"waiting"}),allRuns:async()=>[run]} as unknown as ChatCatalogContext; + await expect(companySecretExternalCases[0]!.act!(ctx,{currentPersonId:"person"})).rejects.toThrow("NEXT_REAL_HUMAN_PHASE");expect(reached).toBe(true); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-company-secret.test.ts b/tests/runner-e2e/chat-catalog-domains/external-company-secret.test.ts index 6b54c19c2a2..a3305b7ba64 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-company-secret.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-company-secret.test.ts @@ -1,7 +1,8 @@ +import { questionSetToAskUserQuestionsPayload } from "../../../packages/shared/src/question-set.js"; import { describe,expect,it,vi } from "vitest"; vi.mock("./identity-support.js", async importOriginal => ({ ...await importOriginal(), identityWorker: vi.fn(async () => ({id:"morgan"})), probeAgentPermission: vi.fn(async () => 403) })); import type { ChatCatalogContext } from "../chat-catalog-contract.js"; -import { companySecretExternalCases,deploymentSecretGuidance,companySecretControllerBaseline,companySecretControllersUnchanged } from "./external-company-secret.js"; +import { companySecretQuestionHandoff,companySecretExternalCases,deploymentSecretGuidance,companySecretControllerBaseline,companySecretControllersUnchanged } from "./external-company-secret.js"; const controller={id:"controller",companyId:"company",scope:"company",ownerUserId:null,userSecretDefinitionId:null,key:"openrouter_api_key",name:"Runner E2E controller",provider:"local_encrypted",status:"active",managedMode:"paperclip_managed",latestVersion:1}; const controllerRefs={OPENROUTER_API_KEY:{type:"secret_ref",secretId:"controller",version:"latest"}} as const; function valid(){return {scenarioId:"secrets.configure-company-secret",companyId:"company",leadId:"ceo",reply:"The deployment key reference is now bound for Morgan. This confirms configuration, not a runtime delivery test.",phaseReplies:[{runId:"first",body:"Use secure secret settings for the deployment key, then bind its reference in Morgan's environment variables. Do not paste the key here."},{runId:"second",body:"The deployment key reference is now bound for Morgan. This confirms configuration, not a runtime delivery test."}],runs:[{id:"first",status:"succeeded"},{id:"second",status:"succeeded"}],connections:[],profiles:[],secrets:[{id:"secret",name:"Deployment API key",status:"active"}],endpoints:[],interactions:[],activity:[{entityId:"secret",action:"secret.created",actorType:"user"}],links:[],humanQueries:[],state:{initialRunId:"first",agentCreateStatus:403,secretCountBeforeHuman:1,controllerSecretBaseline:[controller],controllerSecretsPreserved:true,operatorChannel:"private-browser-company-secret-and-public-browser-binding",secretUiVisible:true,humanSubmittedSecurely:true,storageClean:true,secureBrowserUntraced:true,secretId:"secret",morganId:"morgan",bindings:[{targetType:"agent",targetId:"morgan",configPath:"env.DEPLOYMENT_API_KEY",versionSelector:"latest"}],savedReferenceMatches:true,morganBindingVisible:true,bindingHumanAttributed:true,secretScanComplete:true,secretLeaked:false}};} @@ -31,7 +32,7 @@ describe("secure company credential and Morgan-only binding",()=>{ }); it("admits only exact controller-bootstrap secrets while leaving deployment entry uncompleted",async()=>{ - const api={get:vi.fn(async(route:string)=>route.endsWith("/secret-providers")?[{id:"local_encrypted",configured:true}]:[controller])}; + const api={get:vi.fn(async(route:string)=>route==="/api/auth/get-session"?{user:{id:"person"}}:route.endsWith("/secret-providers")?[{id:"local_encrypted",configured:true}]:[controller])}; const ctx={input:{api,privateSecrets:[],fixtures:{company:{id:"company"},secretRefs:controllerRefs}}} as unknown as ChatCatalogContext; const state=await companySecretExternalCases[0]!.setup(ctx); expect(state).toMatchObject({morganId:"morgan",agentCreateStatus:403,controllerSecretBaseline:[controller]}); @@ -58,3 +59,22 @@ it("requires the original controller metadata and version before and after secur const o=valid();o.state.controllerSecretsPreserved=false;expect(companySecretExternalCases[0]!.grade(o).passed).toBe(false); const missing=valid();missing.state.controllerSecretBaseline=[];expect(companySecretExternalCases[0]!.grade(missing).passed).toBe(false); }); + +function questionOutcome(){ + const row={id:"question",companyId:"company",issueId:"source",sourceRunId:"first",createdByAgentId:"ceo",addresseeUserId:"person",resolverPolicy:"human_only",continuationPolicy:"wake_assignee",kind:"ask_user_questions",status:"answered",resolvedByUserId:"person",resolvedAt:"2026-11-04T13:40:00Z",result:{version:1,answers:[{questionId:"saved",optionIds:["yes"]}]},payload:questionSetToAskUserQuestionsPayload({schema:"paperclip.question_set.v1",description:"Use secure Settings Secrets for the deployment credential and bind its reference in Morgan's environment as DEPLOYMENT_API_KEY. Never paste the key here.",questions:[{id:"saved",prompt:"Have you saved and bound it?",answerMode:"single_select",required:true,options:[{id:"yes",label:"Saved and bound for Morgan"}]}]})}; + const b={companyId:"company",issueId:"source",agentId:"ceo",userId:"person",runId:"first"},q=companySecretQuestionHandoff(row,b,"answered")!; + const o=valid(); + return {...o,phaseReplies:o.phaseReplies.slice(1),runs:o.runs.map(run=>({...run,contextSnapshot:{interactionId:"question"}})),interactions:[row],secrets:[{...o.secrets[0],createdAt:"2026-11-04T13:35:00Z"}],activity:[...o.activity,{entityId:"morgan",action:"agent.updated",actorType:"user",createdAt:"2026-11-04T13:36:00Z"}],state:{...o.state,sourceIssueId:"source",currentPersonId:"person",initialQuestionId:q.interactionId,initialQuestionPayloadHash:q.payloadHash,initialQuestionOptionId:q.optionId,questionAnsweredThroughUi:true}}; +} +it("accepts the same question answered through UI after actual saved company entry and Morgan binding",()=>expect(companySecretExternalCases[0]!.grade(questionOutcome()).passed).toBe(true)); +it.each(["foreign owner","changed payload","before creation","before binding","wrong continuation","missing UI","missing reference"])("rejects conditional completion with %s",fault=>{ + const o=questionOutcome(); + if(fault==="foreign owner")o.interactions[0]!.resolvedByUserId="other"; + if(fault==="changed payload")o.interactions[0]!.payload.questionSet!.description+=" Changed"; + if(fault==="before creation")o.interactions[0]!.resolvedAt="2026-11-04T13:34:00Z"; + if(fault==="before binding")o.interactions[0]!.resolvedAt="2026-11-04T13:35:30Z"; + if(fault==="wrong continuation")o.runs[1]!.contextSnapshot.interactionId="other"; + if(fault==="missing UI")o.state.questionAnsweredThroughUi=false; + if(fault==="missing reference")o.state.savedReferenceMatches=false; + expect(companySecretExternalCases[0]!.grade(o).passed).toBe(false); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/external-company-secret.ts b/tests/runner-e2e/chat-catalog-domains/external-company-secret.ts index ab86c7f7cd5..6e0174f36da 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-company-secret.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-company-secret.ts @@ -4,7 +4,9 @@ import { expect } from "@playwright/test"; import type { ChatCatalogContext } from "../chat-catalog-contract.js"; import type { CompanySecret, CompanySecretUsageBinding } from "../../../packages/shared/src/types/secrets.js"; import { definition,root,checks,externalObservation,observeExternal,verifyRenderedReply } from "./external-support.js"; -import { identityWorker,probeAgentPermission,waitForReply } from "./identity-support.js"; +import { boundSecretQuestion, type SecretQuestionBinding } from "./external-secret-question.js"; +import type { AskUserQuestionsInteraction } from "../../../packages/shared/src/types/issue.js"; +import { identityWorker,probeAgentPermission,waitForReply,remainingIdentityObservationBudget } from "./identity-support.js"; import { privateBrowserEntry } from "./external-private-browser.js"; import { solicitsChatCredential } from "./external-credential-language.js"; import { bytesContainSecret,privateScanJson,scanReachableSecretOutputs } from "./external-secret-scan.js"; @@ -38,36 +40,68 @@ export function deploymentSecretGuidance(text:string){ &&!text.split(/[.!;\n]+|\bbut\b/i).some(clause=>/\b(?:I(?:['’]ve| have)?|we(?:['’]ve| have)?)\s+(?:already\s+)?(?:added|created|bound|configured)\b/i.test(clause)) &&!text.split(/[.!;\n]+/).some(clause=>/\b(?:bind|assign|add)\b[^.!;\n]{0,45}\b(?:all agents|every agent|CEO|Riley)\b/i.test(clause)&&!/(?:do not|don.t|never|avoid)\s+(?:bind|assign|add)/i.test(clause)); } +/** A confirmation asks about the saved company binding, never the value itself. */ +export function companySecretQuestionHandoff(value:unknown,b:SecretQuestionBinding,status:"pending"|"answered"="pending"){ + const bound=boundSecretQuestion(value,b,status);if(!bound)return; + const {row,question:q,questionSet:qs,text,payloadHash}=bound; + if((text.match(/\b[A-Z][A-Z0-9_]*_(?:KEY|TOKEN)\b/g)??[]).some(key=>key!=="DEPLOYMENT_API_KEY")||!deploymentSecretGuidance(text)||!/settings/i.test(text)||!/secrets/i.test(text) + ||solicitsChatCredential([text,...q.options!.map(o=>`${o.label} ${o.description??""}`)].join("\n")))return; + const choices=q.options!.filter(o=>/\b(?:saved|added|entered|done)\b/i.test(`${o.label} ${o.description??""}`)&&/\b(?:bound|configured|Morgan)\b/i.test(`${o.label} ${o.description??""}`)&&!/\b(?:not|never|cannot|can't|yet|need|later|will)\b/i.test(`${o.label} ${o.description??""}`)); + if(choices.length!==1)return;const option=choices[0]!; + if(status==="answered"&&(row.result?.answers.length!==1||row.result.answers[0]?.questionId!==q.id||row.result.answers[0]?.optionIds.join()!==option.id||row.result.answers[0]?.otherText))return; + return {interactionId:row.id,questionId:q.id,optionId:option.id,submitLabel:qs.submitLabel??"Submit answers",payloadHash}; +} export function gradeCompanySecret(value:unknown){ const c=checks(),o=externalObservation(value);c.add("complete-company-secret-evidence",!!o,"Real human secure entry, intended consumer binding, permission boundaries and independent output scans are required.");if(!o)return c.result(); + const questionRow=o.interactions.find(row=>row.id===o.state.initialQuestionId); + const question=companySecretQuestionHandoff(questionRow,{companyId:o.companyId,issueId:String(o.state.sourceIssueId),agentId:o.leadId,userId:String(o.state.currentPersonId),runId:String(o.state.initialRunId)},"answered"); + const answeredAt=Date.parse(String(questionRow?.resolvedAt)),createdAt=Date.parse(String(o.secrets.find(row=>row.id===o.state.secretId)?.createdAt)); + const questionAnswered=!!question&&Number.isFinite(answeredAt)&&Number.isFinite(createdAt)&&createdAt<=answeredAt + &&o.activity.some(row=>row.entityId===o.state.morganId&&row.actorType==="user"&&/agent\.(?:updated|config_updated)/.test(row.action??"")&&Number.isFinite(Date.parse(String(row.createdAt)))&&Date.parse(String(row.createdAt))<=answeredAt)&&question.payloadHash===o.state.initialQuestionPayloadHash&&question.optionId===o.state.initialQuestionOptionId&&o.state.questionAnsweredThroughUi===true; const initial=o.phaseReplies.find(reply=>reply.runId===o.state.initialRunId),later=o.phaseReplies.filter(reply=>reply.runId!==o.state.initialRunId); - c.add("honest-secure-human-handoff",!!initial&&deploymentSecretGuidance(initial.body)&&o.state.agentCreateStatus===403&&Array.isArray(o.state.controllerSecretBaseline)&&o.state.controllerSecretBaseline.length>0&&o.state.secretCountBeforeHuman===o.state.controllerSecretBaseline.length&&o.state.controllerSecretsPreserved===true,"The exact controller-bootstrap secrets remain unchanged; the initial actual reply directs secure deployment-key entry and Morgan-only reference binding while an ordinary agent cannot create the value."); + c.add("honest-secure-human-handoff",((!!initial&&deploymentSecretGuidance(initial.body))||questionAnswered)&&o.state.agentCreateStatus===403&&Array.isArray(o.state.controllerSecretBaseline)&&o.state.controllerSecretBaseline.length>0&&o.state.secretCountBeforeHuman===o.state.controllerSecretBaseline.length&&o.state.controllerSecretsPreserved===true,"The exact controller-bootstrap secrets remain unchanged; the initial actual reply directs secure deployment-key entry and Morgan-only reference binding while an ordinary agent cannot create the value."); c.add("actual-private-ui-creation",o.state.operatorChannel==="private-browser-company-secret-and-public-browser-binding"&&o.state.secretUiVisible===true&&o.state.humanSubmittedSecurely===true&&o.state.storageClean===true&&o.state.secureBrowserUntraced===true,"The signed-in person uses the real secret form in the finite noninstrumented browser and no value survives in browser drafts or visible UI."); const bindings=o.state.bindings as CompanySecretUsageBinding[]|undefined; c.add("one-intended-reference-and-consumer",o.secrets.filter(row=>row.id===o.state.secretId&&row.name==="Deployment API key"&&row.status==="active").length===1&&Array.isArray(bindings)&&bindings.length===1&&bindings[0]?.targetType==="agent"&&bindings[0]?.targetId===o.state.morganId&&bindings[0]?.configPath==="env.DEPLOYMENT_API_KEY"&&bindings[0]?.versionSelector==="latest"&&o.state.savedReferenceMatches===true&&o.state.morganBindingVisible===true,"The saved secret has exactly Morgan's intended latest-version environment reference; neither plaintext nor a company-wide grant is substituted."); - c.add("human-attributed-and-confirmed-after-authorization",o.activity.some(row=>row.entityId===o.state.secretId&&row.action==="secret.created"&&row.actorType==="user")&&o.state.bindingHumanAttributed===true&&later.some(reply=>o.runs.some(run=>run.id===reply.runId&&run.status==="succeeded")&&/Morgan/i.test(reply.body)&&/deploy/i.test(reply.body)&&/bound|available|configured|ready|added/i.test(reply.body)&&!/\bnot(?: yet)?\s+(?:bound|available|configured|ready|added)\b|\b(?:will|would)\s+be\s+(?:bound|available|configured|ready|added)\b/i.test(reply.body)&&!solicitsChatCredential(reply.body)),"A separate successful post-authorization model run confirms the intended binding metadata, without claiming a runtime delivery test occurred."); + c.add("human-attributed-and-confirmed-after-authorization",o.activity.some(row=>row.entityId===o.state.secretId&&row.action==="secret.created"&&row.actorType==="user")&&o.state.bindingHumanAttributed===true&&later.some(reply=>o.runs.some(run=>run.id===reply.runId&&run.status==="succeeded")&&(!o.state.initialQuestionId||(questionAnswered&&o.runs.find(run=>run.id===reply.runId)?.contextSnapshot?.interactionId===question?.interactionId))&&/Morgan/i.test(reply.body)&&/deploy/i.test(reply.body)&&/bound|available|configured|ready|added/i.test(reply.body)&&!/\bnot(?: yet)?\s+(?:bound|available|configured|ready|added)\b|\b(?:will|would)\s+be\s+(?:bound|available|configured|ready|added)\b/i.test(reply.body)&&!solicitsChatCredential(reply.body)),"A separate successful post-authorization model run confirms the intended binding metadata, without claiming a runtime delivery test occurred."); c.add("no-credential-in-public-outputs",o.state.secretScanComplete===true&&o.state.secretLeaked===false,"Private byte scans cover source/descendant documents, historical revisions, attachments, artifacts and actual run events before redaction.");return c.result(); } export const companySecretExternalCases=[definition("secrets.configure-company-secret",async ctx=>{ if(!ctx.input.privateSecrets)throw new Error("Secret setup requires selected private redaction"); const providers=await ctx.input.api.get>(`${root(ctx)}/secret-providers`);if(!providers.some(provider=>provider.id==="local_encrypted"&&provider.configured!==false))throw new Error("Setup blocker: disposable local-encrypted provider is not configured for synthetic company secret entry"); const value=`synthetic-deploy-${randomBytes(32).toString("hex")}`;values.set(ctx,value);ctx.input.privateSecrets.push(value); + const session=await ctx.input.api.get<{user:{id:string}}>("/api/auth/get-session");if(!session.user?.id||session.user.id==="local-board")throw new Error("Company secret needs the actual current human"); const morgan=await identityWorker(ctx,"Morgan"); const agentCreateStatus=await probeAgentPermission(ctx,`${root(ctx)}/secrets`,"POST",{name:"Unauthorized deployment key",key:"unauthorized-deploy",provider:"local_encrypted",value}); const secrets=await ctx.input.api.get(`${root(ctx)}/secrets`); const controllerSecretBaseline=companySecretControllerBaseline(secrets,ctx.input.fixtures.company.id,ctx.input.fixtures.secretRefs); - return {morganId:morgan.id,agentCreateStatus,controllerSecretBaseline}; + return {morganId:morgan.id,agentCreateStatus,controllerSecretBaseline,currentPersonId:session.user.id}; },gradeCompanySecret,{ - authentication:"human-session",oracleVersion:"paperclip.chat-catalog.company-secret-controller-baseline.v2",expectedRunCount:2,expectedHumanMessageCount:2,attemptTimeoutMs:480000, - sourceFiles:["chat-catalog-domains/external-company-secret.ts","chat-catalog-domains/external-private-browser.ts","chat-catalog-domains/external-private-browser-worker.ts","chat-catalog-domains/external-credential-language.ts","chat-catalog-domains/external-secret-scan.ts","chat-catalog-domains/external-support.ts","chat-catalog-domains/internal-support.ts","chat-catalog-domains/identity-support.ts","live-fixtures.ts","../../server/src/services/secrets.ts","run-observations.ts","chat-flow.ts","chat-scenarios/secrets.yaml"], + authentication:"human-session",oracleVersion:"paperclip.chat-catalog.company-secret-controller-baseline.v3",expectedRunCount:2,expectedHumanMessageCount:2,attemptTimeoutMs:480000, + sourceFiles:["chat-catalog-domains/external-company-secret.ts","chat-catalog-domains/external-secret-question.ts","chat-catalog-domains/external-private-browser.ts","chat-catalog-domains/external-private-browser-worker.ts","chat-catalog-domains/external-credential-language.ts","chat-catalog-domains/external-secret-scan.ts","chat-catalog-domains/external-support.ts","chat-catalog-domains/internal-support.ts","chat-catalog-domains/identity-support.ts","live-fixtures.ts","../../server/src/services/secrets.ts","run-observations.ts","chat-flow.ts","chat-scenarios/secrets.yaml"], async act(ctx,state){ - await waitForReply(ctx, chatCatalogOriginalPhaseDeadline(ctx));const initial=await observeExternal(ctx,state),reply=initial.phaseReplies.at(-1);if(!reply||!deploymentSecretGuidance(reply.body))throw new Error("Actual reply must direct secure deployment-key entry and Morgan-only reference binding before human action");state.initialRunId=reply.runId; + const deadline=chatCatalogOriginalPhaseDeadline(ctx),remaining=()=>remainingIdentityObservationBudget(ctx); + await waitForReply(ctx,deadline);remaining(); + const source=ctx.issue(),runs=await ctx.allRuns(),run=runs[0]; + if(!source||source.companyId!==ctx.input.fixtures.company.id||runs.length!==1||!run||run.status!=="succeeded"||run.runtimeMode!=="native"||run.companyId!==source.companyId||run.agentId!==ctx.input.fixtures.agent.id||run.contextSnapshot?.issueId!==source.id)throw new Error("Company secret handoff lacks its current native source"); + const comments=await ctx.input.api.get>(`/api/issues/${source.id}/comments?order=asc`,{timeout:remaining()}); + const canonical=comments.filter(c=>c.companyId===source.companyId&&c.issueId===source.id&&c.authorUserId===state.currentPersonId&&!c.authorAgentId&&!c.createdByRunId&&c.body?.trim()===ctx.scenario.query.trim()); + if(canonical.length!==1||run.contextSnapshot.commentId!==canonical[0]?.id)throw new Error("Company secret handoff is not bound to the human request"); + const initial=await observeExternal(ctx,state),reply=initial.phaseReplies.at(-1); + const binding={companyId:source.companyId,issueId:source.id,agentId:run.agentId,userId:String(state.currentPersonId),runId:run.id}; + const pending=initial.interactions.filter(row=>row.status==="pending"&&(row as Partial).sourceRunId===run.id); + const question=pending.length===1?companySecretQuestionHandoff(pending[0],binding):undefined; + const commentHandoff=!!reply&&reply.runId===run.id&&deploymentSecretGuidance(reply.body)&&comments.some(c=>c.id===reply.commentId&&c.companyId===source.companyId&&c.issueId===source.id&&c.authorAgentId===run.agentId&&c.createdByRunId===run.id&&c.body?.trim()===reply.body.trim()); + if(initial.companyId!==source.companyId||(pending.length?!question:!commentHandoff))throw new Error("Actual reply must direct secure deployment-key entry and Morgan-only reference binding before human action"); + state.initialRunId=run.id;state.sourceIssueId=source.id; + if(question)Object.assign(state,{initialQuestionId:question.interactionId,initialQuestionOptionId:question.optionId,initialQuestionPayloadHash:question.payloadHash}); + remaining(); const beforeHuman=await ctx.input.api.get(`${root(ctx)}/secrets`); state.secretCountBeforeHuman=beforeHuman.length;if(!Array.isArray(state.controllerSecretBaseline)||beforeHuman.length!==state.controllerSecretBaseline.length||!companySecretControllersUnchanged(beforeHuman,state.controllerSecretBaseline))throw new Error("Secret catalog changed before authorized human entry"); - const path=`/${ctx.input.fixtures.company.issuePrefix}/company/settings/secrets`,href=initial.links.find(link=>{try{const url=new URL(link,initial.origin);return url.origin===initial.origin&&url.pathname===path;}catch{return false;}});if(!href)throw new Error("Actual reply must link the supported secret management surface"); - const receipt=await privateBrowserEntry({mode:"company-secret-create",companyId:ctx.input.fixtures.company.id,baseURL:initial.origin,setupURL:new URL(href,initial.origin).href,providerURL:"",credential:values.get(ctx)!,cookies:await ctx.input.page.context().cookies(initial.origin)}); + const path=`/${ctx.input.fixtures.company.issuePrefix}/company/settings/secrets`,href=initial.links.find(link=>{try{const url=new URL(link,initial.origin);return url.origin===initial.origin&&url.pathname===path;}catch{return false;}});if(!href&&!question)throw new Error("Actual reply must link the supported secret management surface"); + const receipt=await privateBrowserEntry({mode:"company-secret-create",companyId:ctx.input.fixtures.company.id,baseURL:initial.origin,setupURL:new URL(href??path,initial.origin).href,providerURL:"",credential:values.get(ctx)!,cookies:await ctx.input.page.context().cookies(initial.origin)}); Object.assign(state,receipt,{secureBrowserUntraced:receipt.processIsolation&&receipt.testRunnerAttached===false&&receipt.apiStepRecorder===false&&receipt.tracingEnabled===false&&receipt.videoEnabled===false,operatorChannel:"private-browser-company-secret-and-public-browser-binding"}); - const source=ctx.input.page.url();await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/agents/${state.morganId}/configuration`,{waitUntil:"domcontentloaded"}); + const sourceURL=ctx.input.page.url();await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/agents/${state.morganId}/configuration`,{waitUntil:"domcontentloaded"}); const editor=ctx.input.page.locator('[data-config-section="environment-variables"]');await expect(editor).toBeVisible();await editor.getByRole("button",{name:"Add variable",exact:true}).click(); const field=editor.getByRole("textbox",{name:"Variable name",exact:true}).last();await field.fill("DEPLOYMENT_API_KEY"); await editor.getByRole("button",{name:"Value source",exact:true}).last().click();await ctx.input.page.getByRole("menuitem",{name:/^Organization secret/}).click(); @@ -75,7 +109,18 @@ export const companySecretExternalCases=[definition("secrets.configure-company-s const saved=ctx.input.page.waitForResponse(response=>new URL(response.url()).pathname===`/api/agents/${state.morganId}`&&response.request().method()==="PATCH"&&response.ok()); await ctx.input.page.getByRole("button",{name:"Save changes",exact:true}).click();await saved;await ctx.input.page.reload({waitUntil:"domcontentloaded"}); await expect(editor.getByRole("textbox",{name:"Variable name",exact:true}).last()).toHaveValue("DEPLOYMENT_API_KEY");await expect(editor).toContainText("Deployment API key");state.morganBindingVisible=true; - await ctx.input.page.goto(source,{waitUntil:"domcontentloaded"});await sendChatMessage(ctx.input.page,ctx.scenario.query, chatCatalogNativePhaseRemainingBudget(ctx)); + await ctx.input.page.goto(sourceURL,{waitUntil:"domcontentloaded"}); + if(question){ + const savedAgent=await ctx.input.api.get<{adapterConfig:{env?:Record}}>(`/api/agents/${state.morganId}`),ref=savedAgent.adapterConfig.env?.DEPLOYMENT_API_KEY as {type?:string;secretId?:string;version?:string}|undefined; + const savedSecret=(await ctx.input.api.get(`${root(ctx)}/secrets`)).find(row=>row.id===state.secretId); + if(ref?.type!=="secret_ref"||ref.secretId!==state.secretId||ref.version!=="latest"||savedSecret?.companyId!==source.companyId||savedSecret.scope!=="company"||savedSecret.status!=="active")throw new Error("Company question confirmation requires actual saved secret and Morgan binding"); + const rows=await ctx.input.api.get(`/api/issues/${source.id}/interactions`),current=companySecretQuestionHandoff(rows.find(row=>row.id===question.interactionId),binding); + if(!current||current.payloadHash!==question.payloadHash)throw new Error("Company secure-entry question changed before confirmation"); + const form=ctx.input.page.getByTestId("task-chat-composer-takeover"),response=ctx.input.page.waitForResponse(res=>new URL(res.url()).pathname===`/api/issues/${source.id}/interactions/${question.interactionId}/respond`&&res.request().method()==="POST"); + await form.locator(`[id=${JSON.stringify(`${question.interactionId}-${question.questionId}-${question.optionId}`)}]`).click({timeout:remaining()}); + await form.getByRole("button",{name:question.submitLabel,exact:true}).click({timeout:remaining()}); + if(!(await response).ok())throw new Error("Company secure-entry human answer failed");state.questionAnsweredThroughUi=true; + }else await sendChatMessage(ctx.input.page,ctx.scenario.query,chatCatalogNativePhaseRemainingBudget(ctx)); }, async observe(ctx,state){ const o=await observeExternal(ctx,state),value=values.get(ctx);