Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to Doable Agent Plugins are documented here.

## [0.2.7] - 2026-09-09

### Fixed

- Validate mapped opaque repository references as identifiers so a checkout named
`repo` can connect and submit without weakening repository-name privacy checks.
- Keep specific behavior questions focused on their execution path and material
conditions; retain flow and sibling coverage for feature-wide testing requests.
- Check concrete fixture values against validation and guards before asserting
their outcome; keep illustrative journeys out of unrelated behavior answers.
- Require candidate repairs without modifying helper privacy or transport checks.

## [0.2.6] - 2026-09-07

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Official agent plugins for [Doable](https://getdoable.ai), supporting Codex, Cla

| Plugin | Version | Purpose | Network |
| --- | --- | --- | --- |
| `doable-code-context` | `0.2.6` | Resolve context requests or start a managed feature-testing workflow | Doable MCP |
| `doable-code-context` | `0.2.7` | Resolve context requests or start a managed feature-testing workflow | Doable MCP |

## Workflow

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doable-agent-plugins",
"version": "0.2.6",
"version": "0.2.7",
"private": true,
"description": "Official installable agent plugins for Doable.",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion plugins/doable-code-context/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doable-code-context",
"version": "0.2.6",
"version": "0.2.7",
"description": "Connect private code to Doable through MCP, resolve grounded context requests, and start managed feature-testing workflows.",
"author": {
"name": "Doable AI",
Expand Down
2 changes: 1 addition & 1 deletion plugins/doable-code-context/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doable-code-context",
"version": "0.2.6",
"version": "0.2.7",
"description": "Connect private code to Doable through MCP, resolve grounded context requests, and start managed feature-testing workflows.",
"author": {
"name": "Doable AI",
Expand Down
2 changes: 1 addition & 1 deletion plugins/doable-code-context/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "doable-code-context",
"displayName": "Doable Code Context",
"version": "0.2.6",
"version": "0.2.7",
"description": "Connect private code to Doable through MCP, resolve grounded context requests, and start managed feature-testing workflows.",
"author": {
"name": "Doable AI"
Expand Down
11 changes: 10 additions & 1 deletion plugins/doable-code-context/scripts/doable-code-context.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
import { execFileSync } from "node:child_process";

const CLIENT = Object.freeze({ name: "doable-code-context", version: "0.2.3" });
const CLIENT = Object.freeze({ name: "doable-code-context", version: "0.2.7" });
const STATE_SCHEMA_VERSION = "1";
const SUBMISSION_SCHEMA_VERSION = "1";

Expand Down Expand Up @@ -533,6 +533,15 @@ function assertRemotePayloadSafe(value, state, label = "remote payload", key = "
assertRemotePayloadSafe(childValue, state, `${label}.${childKey}`, childKey);
}
} else if (typeof value === "string") {
if (key === "repo_ref") {
// This is a typed, mapped identifier, not prose containing a repository name.
// Checking it as prose rejects every repo_ ID when the checkout is named repo.
assert(
OPAQUE_REPO_RE.test(value) && state.repositories.some((repository) => repository.repoRef === value),
`${label} must reference a mapped opaque repository`,
);
return;
}
assertSafeText(value, label, state, { max: 8_000 });
}
}
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/verify-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:
const plugins = [
{
name: "doable-code-context",
version: "0.2.6",
version: "0.2.7",
skillNames: ["doable-connect", "doable-answer-questions", "doable-test-feature"],
network: "bundled-doable-mcp-config",
},
Expand Down
24 changes: 23 additions & 1 deletion tests/doable-code-context-helper.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ function runHelper(args, env) {
test("connected helper preserves the local/private boundary and retries idempotently", async (t) => {
const testRoot = mkdtempSync(join(tmpdir(), "doable-code-context-test-"));
t.after(() => rmSync(testRoot, { recursive: true, force: true }));
const repository = join(testRoot, "private-admin-repository");
// A generic checkout name must not collide with the opaque repo_ ID prefix.
const repository = join(testRoot, "repo");
mkdirSync(repository);
execFileSync("git", ["init", "-q", repository]);
execFileSync("git", ["-C", repository, "config", "user.email", "test@example.invalid"]);
Expand Down Expand Up @@ -312,6 +313,27 @@ test("connected helper preserves the local/private boundary and retries idempote
writeFileSync(submissionPath, `${JSON.stringify(submission, null, 2)}\n`);
chmodSync(submissionPath, 0o600);

for (const statement of [
"The repo contains the Save control.",
"The private-admin-repository contains the Save control.",
]) {
const leakingFinding = structuredClone(submission);
leakingFinding.answers[0].findings[0].statement = statement;
writeFileSync(submissionPath, `${JSON.stringify(leakingFinding, null, 2)}\n`);
await assert.rejects(
runHelper(["validate-submission", "--state", statePath, "--candidate", submissionPath], environment),
/exposes local repository provenance/i,
);
}

const unmappedEvidence = structuredClone(submission);
unmappedEvidence.evidence[0].repoRef = "repo_00000000";
writeFileSync(submissionPath, `${JSON.stringify(unmappedEvidence, null, 2)}\n`);
await assert.rejects(
runHelper(["validate-submission", "--state", statePath, "--candidate", submissionPath], environment),
/unknown repository/i,
);

const orderedJourney = structuredClone(submission);
Object.assign(orderedJourney.answers[0].findings[0], {
journeyRef: "j_submit_promotion",
Expand Down
Loading