From 64b1de21b5702228815a65b544ef45be5610e176 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 11:09:51 +0000 Subject: [PATCH 1/3] fix: require scope for GitOps agent deletion Co-authored-by: Rohan Gupta --- src/registry/toolsets/gitops.ts | 9 ++++---- tasks/todo.md | 12 ++++++++++ tests/registry/gitops.test.ts | 33 ++++++++++++++++++++++++++- tests/tools/tool-handlers.test.ts | 37 +++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/registry/toolsets/gitops.ts b/src/registry/toolsets/gitops.ts index 9c9d39fbb..7c1fd125f 100644 --- a/src/registry/toolsets/gitops.ts +++ b/src/registry/toolsets/gitops.ts @@ -228,13 +228,14 @@ export const gitopsToolset: ToolsetDefinition = { responseExtractor: passthrough, description: "Delete a GitOps agent. The agent identifier in the path is the raw identifier — NOT scope-prefixed.\n\n" + + "resource_scope is required because the same raw agent identifier can exist at account, org, and project scope.\n" + "SCOPE DETERMINES WHICH ORG/PROJECT PARAMS ARE INJECTED:\n" + " Account-level agent: resource_scope='account' — omit org_id and project_id\n" + " harness_delete(resource_type='gitops_agent', resource_id='myagent', resource_scope='account')\n" + " Org-level agent: resource_scope='org' — org_id is injected, no project_id\n" + " harness_delete(resource_type='gitops_agent', resource_id='myagent', resource_scope='org')\n" + " Project-level agent: resource_scope='project' — both org_id and project_id are injected\n" + - " harness_delete(resource_type='gitops_agent', resource_id='myagent')\n\n" + + " harness_delete(resource_type='gitops_agent', resource_id='myagent', resource_scope='project')\n\n" + "NOTE: The path identifier is always the raw agent ID (e.g. 'myagent'), never scope-prefixed\n" + "(unlike other GitOps APIs where agentIdentifier is prefixed with 'account.', 'org.', etc.).\n" + "The backend derives the scope from the presence of orgIdentifier and projectIdentifier in the request.", @@ -250,12 +251,12 @@ export const gitopsToolset: ToolsetDefinition = { }, { name: "resource_scope", - required: false, + required: true, description: - "Controls which scope params are injected. " + + "Required top-level harness_delete field that controls which scope params are injected. " + "'account' — account-level agent (no org/project). " + "'org' — org-level agent (orgIdentifier injected). " + - "'project' — project-level agent (orgIdentifier + projectIdentifier injected, default when omitted).", + "'project' — project-level agent (orgIdentifier + projectIdentifier injected).", }, ], } satisfies ParamsSchema, diff --git a/tasks/todo.md b/tasks/todo.md index ca802b154..1e8b8c676 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,5 +1,17 @@ # Harness MCP Server — Task Tracking +## Critical Bug Investigation Automation (2026-07-20) +- [x] Baseline branch state and identify recent behavioral commits +- [x] Review high-blast-radius diffs and trace concrete trigger scenarios +- [x] Implement a minimal fix only if a critical bug is proven +- [ ] Validate any fix, or complete targeted sanity checks for a no-fix outcome +- [ ] Commit/push/open PR if fixed; otherwise report no critical bugs in Slack + +### Plan +- Review changes merged after the prior 2026-07-13 investigation, prioritizing runtime behavior over release metadata. +- Trace candidate failures through public tool handlers, registry dispatch, request construction, and response extraction. +- Require a concrete crash, data-loss, security, or major user-breakage trigger before modifying runtime code. + ## Version Bump 3.2.12 (2026-07-19) - [x] Update package and bundle manifest versions to 3.2.12 - [x] Update the release metadata version test diff --git a/tests/registry/gitops.test.ts b/tests/registry/gitops.test.ts index 712828b29..48f3b5f22 100644 --- a/tests/registry/gitops.test.ts +++ b/tests/registry/gitops.test.ts @@ -81,17 +81,48 @@ describe("gitops_agent", () => { expect(call.path).toBe("/gitops/api/v1/agents/account.myagent"); }); - it("delete: raw agent_id → DELETE /gitops/api/v1/agents/{agentIdentifier}", async () => { + it("delete: requires an explicit resource_scope", async () => { + const mockRequest = vi.fn(); + const client = makeClient(mockRequest); + + await expect( + registry.dispatch(client, "gitops_agent", "delete", { + agent_id: "agent1779094157087", + }), + ).rejects.toThrow(/Missing required param\(s\).*resource_scope/); + expect(mockRequest).not.toHaveBeenCalled(); + }); + + it("delete: project scope injects org and project identifiers", async () => { + const mockRequest = vi.fn().mockResolvedValue({}); + const client = makeClient(mockRequest); + + await registry.dispatch(client, "gitops_agent", "delete", { + agent_id: "agent1779094157087", + resource_scope: "project", + }); + + const call = mockRequest.mock.calls[0][0]; + expect(call.method).toBe("DELETE"); + expect(call.path).toBe("/gitops/api/v1/agents/agent1779094157087"); + expect(call.params.orgIdentifier).toBe("default"); + expect(call.params.projectIdentifier).toBe("test-project"); + }); + + it("delete: account scope omits org and project identifiers", async () => { const mockRequest = vi.fn().mockResolvedValue({}); const client = makeClient(mockRequest); await registry.dispatch(client, "gitops_agent", "delete", { agent_id: "agent1779094157087", + resource_scope: "account", }); const call = mockRequest.mock.calls[0][0]; expect(call.method).toBe("DELETE"); expect(call.path).toBe("/gitops/api/v1/agents/agent1779094157087"); + expect(call.params.orgIdentifier).toBeUndefined(); + expect(call.params.projectIdentifier).toBeUndefined(); }); }); diff --git a/tests/tools/tool-handlers.test.ts b/tests/tools/tool-handlers.test.ts index 3f56b637f..e9b10fbf5 100644 --- a/tests/tools/tool-handlers.test.ts +++ b/tests/tools/tool-handlers.test.ts @@ -1345,6 +1345,43 @@ describe("harness_delete", () => { expect(data.deleted).toBe(true); }); + it("requires explicit scope before deleting a GitOps agent", async () => { + const gitopsRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "gitops" })); + const gitopsServer = makeMcpServer("accept"); + const { registerDeleteTool } = await import("../../src/tools/harness-delete.js"); + registerDeleteTool(gitopsServer, gitopsRegistry, client, makeConfig()); + + const result = await gitopsServer.call("harness_delete", { + resource_type: "gitops_agent", + resource_id: "shared-agent", + }); + + expect(result.isError).toBe(true); + expect(parseResult(result)).toMatchObject({ + error: expect.stringContaining("resource_scope"), + }); + expect(mockRequest).not.toHaveBeenCalled(); + }); + + it("forwards explicit project scope when deleting a GitOps agent", async () => { + const gitopsRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "gitops" })); + const gitopsServer = makeMcpServer("accept"); + const { registerDeleteTool } = await import("../../src/tools/harness-delete.js"); + registerDeleteTool(gitopsServer, gitopsRegistry, client, makeConfig()); + + const result = await gitopsServer.call("harness_delete", { + resource_type: "gitops_agent", + resource_id: "shared-agent", + resource_scope: "project", + }); + + expect(result.isError).toBeUndefined(); + const callArgs = mockRequest.mock.calls[0]![0] as { params: Record; path: string }; + expect(callArgs.path).toBe("/gitops/api/v1/agents/shared-agent"); + expect(callArgs.params.orgIdentifier).toBe("default"); + expect(callArgs.params.projectIdentifier).toBe("test-project"); + }); + it("returns structured delete payload without spreading API fields at top level", async () => { const templateRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "templates" })); const templateServer = makeMcpServer("accept"); From 695b43401e3aabb6aa6ef5b3e7137bb3c5d6804c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 11:11:15 +0000 Subject: [PATCH 2/3] docs: record GitOps deletion investigation Co-authored-by: Rohan Gupta --- tasks/todo.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tasks/todo.md b/tasks/todo.md index 1e8b8c676..3302e54b3 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -4,7 +4,7 @@ - [x] Baseline branch state and identify recent behavioral commits - [x] Review high-blast-radius diffs and trace concrete trigger scenarios - [x] Implement a minimal fix only if a critical bug is proven -- [ ] Validate any fix, or complete targeted sanity checks for a no-fix outcome +- [x] Validate any fix, or complete targeted sanity checks for a no-fix outcome - [ ] Commit/push/open PR if fixed; otherwise report no critical bugs in Slack ### Plan @@ -12,6 +12,11 @@ - Trace candidate failures through public tool handlers, registry dispatch, request construction, and response extraction. - Require a concrete crash, data-loss, security, or major user-breakage trigger before modifying runtime code. +### Review +- Found a wrong-scope deletion hazard in the newly added `gitops_agent` delete operation. GitOps agent paths use raw, non-scope-prefixed IDs, and the backend chooses account/org/project scope from query params. Omitting `resource_scope` therefore sent an account-scoped DELETE, while the operation's project example explicitly omitted the field. If the same ID existed at account and project scope, a project-agent deletion request could delete the account agent. +- Changed the operation to require explicit `resource_scope`, corrected every example, and added registry plus public `harness_delete` regressions for fail-closed omission and exact account/project query scoping. +- Verification passed: focused GitOps deletion regressions (16 tests), complete GitOps/tool-handler suites (248 tests), `pnpm typecheck`, `pnpm build`, `pnpm test` (117 files / 2557 tests), `pnpm standards:check` (80 tests), `pnpm docs:check`, and `git diff --check HEAD`. + ## Version Bump 3.2.12 (2026-07-19) - [x] Update package and bundle manifest versions to 3.2.12 - [x] Update the release metadata version test From d696331e3b5dc40cd2cfb2eb22fcb3ecfe212f82 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 11:12:20 +0000 Subject: [PATCH 3/3] docs: complete critical bug investigation Co-authored-by: Rohan Gupta --- tasks/todo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/todo.md b/tasks/todo.md index 3302e54b3..7a8e2508e 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -5,7 +5,7 @@ - [x] Review high-blast-radius diffs and trace concrete trigger scenarios - [x] Implement a minimal fix only if a critical bug is proven - [x] Validate any fix, or complete targeted sanity checks for a no-fix outcome -- [ ] Commit/push/open PR if fixed; otherwise report no critical bugs in Slack +- [x] Commit/push/open PR if fixed; otherwise report no critical bugs in Slack ### Plan - Review changes merged after the prior 2026-07-13 investigation, prioritizing runtime behavior over release metadata.