fix(agents): a model-supplied id must not be a LIKE wildcard - #196
Merged
Merged
Conversation
`memory pin`, `memory delete` and `skills delete` interpolate their
positional argument straight into a LIKE pattern:
path LIKE `memory/%/${id}.md`
path LIKE `skills/${name}/%`
`%` and `_` are wildcards there, and the argument comes from the model.
`cumora memory delete %` becomes `path LIKE 'memory/%/%.md'` and removes
every memory the agent has, then reports "deleted %". Verified against
Postgres 16: three memories in, DELETE 3, none left. `_` is the quieter
version — one character wide, so `mem-aaaaaaaa-11_` hits a real id the
caller never named. `skills delete %` is the same shape one command over.
Escape rather than reject: skill names are free text, and Postgres's
default LIKE escape is a backslash reaching it as a bound parameter, so
no ESCAPE clause is needed. Confirmed both directions — an escaped `%`
stops matching arbitrary content, and a skill genuinely named "100%"
still deletes itself and nothing else.
Second defect in the same family, fixed here because it is the other
half of the documented workflow: `memory list` printed
`m.id.slice(0, 10)` while ids are minted 16 wide (`mem-` + 12 characters
of a UUID). So the token the listing showed could never resolve —
`memory/%/mem-c2ad15.md` does not match
`memory/observation/mem-c2ad155a-56a.md`. `cumora help` documents
list → pin/delete as the workflow, and it could not round-trip for any
memory ever written. Print the full id.
Four of the six integration cases go red against the shipped code. The
two that pass either way are the guards: an exact id still resolves, and
a literal `%` in a name still matches itself.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cumora memory delete %deletes every memory the agent has, and reports success.Three commands interpolate their positional argument straight into a LIKE pattern:
%and_are wildcards to LIKE, and the argument comes from the model. There is no validation betweenparsed.positional[1]and the query.Measured
Postgres 16, three memories for one agent, running exactly what the command issues:
_is the quieter one — it matches exactly one character, somem-aaaaaaaa-11_resolves to a real memory the caller never named, anddeleted mem-aaaaaaaa-11_looks like it did what was asked.Scope is the agent's own rows (
agent_id = $1is parameterised, and both statements are tenant-bound), so this is data loss inside one agent rather than a boundary crossing. That is also why it is easy to miss: nothing about it looks dangerous from outside the agent.Escape, don't reject
Skill names are free text, so a shape check would be wrong for them. Postgres's default LIKE escape is a backslash, and the pattern arrives as a bound parameter, so escaping in TypeScript is sufficient and no
ESCAPEclause is needed. I verified both directions against the database rather than assuming:The helper escapes the backslash itself too, so it cannot be used to escape something else.
The other half of the same workflow
memory listprinted a truncated id:` ${pin}[${m.id.slice(0, 10)}] …`while ids are minted 16 wide —
mem-plus 12 characters of a UUID, e.g.mem-c2ad155a-56a, shown asmem-c2ad15. Both mutating commands resolve an exact file stem, somemory/%/mem-c2ad15.mdcannot matchmemory/observation/mem-c2ad155a-56a.md:cumora helpdocumentsmemory list→memory pin <id>/memory delete <id>as the workflow, and it could not round-trip for any memory ever written. Fixed here because it is the same command family and the same round trip — an agent that copies what the tool printed is the intended use.Verification
Six integration cases driving the real
runCli. Four go red against the shipped code:The two that pass either way are the guards against over-fixing: an exact id still pins and deletes, and a skill genuinely named
100%still deletes itself and leaves its sibling alone.Unit suite 1102 pass / 0 fail; the existing
agent-cli-side-effectsintegration suite stays green;tsc --noEmit,biome lint ., all three source guards clean.Noted, not changed
memory list --kindbuildsmemory/${k}/%from another unescaped argument (cli.ts:4037-4038). That one only widens a read, so it cannot destroy anything, and narrowing it changes what a listing returns — worth a separate look rather than folding into a data-loss fix.