diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 154129a..d54ee3e 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -316,7 +316,7 @@

Azure DevOps intake (optional)

Video evidence records only the application journey and remains local under repository-owned Playwright artifact directories. Screenshots are still required for lightweight review evidence.

Playwright validation only skips source diagnosis, implementation, review, and pull-request work. It performs required profile setup, reuses browser authentication, starts required applications, and runs the focused browser journey.

- +
Access scope diff --git a/docs/commands.md b/docs/commands.md index 266c0db..0df2f4c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -499,6 +499,14 @@ approval, warning and sandbox-escalation checks. Eligible native requests also offer **Allow repository-file reads for this job** or **Allow this test command for this job**, each with a separate confirmation. +Dashboard intake enables **Allow routine source access and checks for this job** +by default. Submitting a fix-and-validate job grants non-sensitive source/test +editing and dashboard-owned validation gates for that job and checkout. +Validation-only modes receive read access, never source/test editing. +Uncheck the option before submission to retain individual approvals. API callers +must still explicitly send `routineFileAccess: true`; omitted or false values +grant no routine access. This default does not enable Allow all tool commands, +override earlier denials, or change existing jobs. Separately, a stopped native job's **Repository file trust** editor can review source/test reads or source/test reads and edits for that exact job and canonical checkout, including resumes and dashboard restart. This setting is not inherited diff --git a/e2e/dashboard-readiness.spec.js b/e2e/dashboard-readiness.spec.js index 665c8e0..924c479 100644 --- a/e2e/dashboard-readiness.spec.js +++ b/e2e/dashboard-readiness.spec.js @@ -753,6 +753,40 @@ test("native Copilot prompts allow once, deny and answer choices in the same liv } }); +test("Copilot intake defaults to source/test editing and trusted validation gates", async ({ page }) => { + const directory = mkdtempSync(join(tmpdir(), "fixlab-default-file-trust-ui-")); + mkdirSync(join(directory, ".github", "fixlab"), { recursive: true }); + writeFileSync(join(directory, ".github", "fixlab", "repository-profile.json"), + readFileSync(join(repository, ".github", "fixlab", "repository-profile.json"))); + let finish; + let execution; + const instance = createDashboardServer({ + repository: directory, packageRoot, runtime: "copilot", browserOpener() {}, + executor(input) { + execution = input; + return { + completion: new Promise((resolve) => { finish = resolve; }), + terminate() { finish({ code: 130 }); }, hasStopped: () => true + }; + } + }); + try { + const address = await instance.listen({ port: await availablePort() }); + await openWorkflowControls(page, address.url); + await expect(page.locator('[name="routineFileAccess"]')).toBeChecked(); + await page.locator("#request").fill("Synthetic default backend test editing."); + await page.locator("#start-button").click(); + await expect(page.locator("#job-file-access-mode")).toHaveValue("source-read-write"); + await expect(page.locator("#job-file-access-gates")).toBeChecked(); + expect(execution.fileAccess.mode).toBe("source-read-write"); + expect(execution.fileAccess.allowValidationGates).toBe(true); + expect(execution.toolApprovalMode).toBe("ask"); + } finally { + await instance.close(); + rmSync(directory, { recursive: true, force: true }); + } +}); + test("Copilot file trust requires a stopped exact job and a separate confirmation without resuming", async ({ page }) => { const directory = mkdtempSync(join(tmpdir(), "fixlab-file-trust-ui-")); mkdirSync(join(directory, ".github", "fixlab"), { recursive: true }); @@ -773,6 +807,7 @@ test("Copilot file trust requires a stopped exact job and a separate confirmatio try { const address = await instance.listen({ port: await availablePort() }); await openWorkflowControls(page, address.url); + await page.locator('[name="routineFileAccess"]').uncheck(); await page.locator("#request").fill("Synthetic reviewed source file trust."); await page.locator("#start-button").click(); await expect(page.locator("#job-file-access-mode")).toHaveValue("manual"); diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index d771403..471706b 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -2316,17 +2316,22 @@ test("reviewed file trust is exact-job bound, persists on resume/restart and is await new Promise((resolve) => setTimeout(resolve, 50)); const fresh = await jsonRequest(running.url, "/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ request: "A different untrusted job.", mode: "validate-only", fileAccess: expected }) + body: JSON.stringify({ request: "A different untrusted job.", mode: "validate-only", + routineFileAccess: false, fileAccess: expected }) }); assert.equal(fresh.response.status, 202, fresh.body.error); const queued = fresh.body.queue.at(-1); assert.notEqual(queued.id, created.body.job.id); assert.equal(queued.fileAccess, null); assert.equal(launches.length, 2, "creating a queued job does not inherit or execute trusted access"); + const intakeHtml = await (await fetch(running.url)).text(); + const routineAccessInput = intakeHtml.match(/]*name="routineFileAccess"[^>]*>/)?.[0]; + assert.ok(routineAccessInput, "dashboard intake must expose the routine access choice"); + const routineFileAccess = /\bchecked\b/.test(routineAccessInput); const submission = await jsonRequest(running.url, "/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request: "Explicit routine source consent.", mode: "fix-and-validate", - routineFileAccess: true }) + routineFileAccess }) }); assert.equal(submission.response.status, 202, submission.body.error); const consented = submission.body.queue.at(-1); @@ -2334,6 +2339,13 @@ test("reviewed file trust is exact-job bound, persists on resume/restart and is assert.equal(consented.fileAccess.jobId, consented.id); assert.equal(consented.fileAccess.repository, realpathSync.native(repository)); assert.equal(consented.fileAccess.allowValidationGates, true); + const validation = await jsonRequest(running.url, "/api/jobs", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ request: "Default access in read-only validation.", mode: "validate-only", + routineFileAccess }) + }); + assert.equal(validation.response.status, 202, validation.body.error); + assert.equal(validation.body.queue.at(-1).fileAccess.mode, "source-read"); } finally { await running.dashboard.close(); rmSync(repository, { recursive: true, force: true });