Repository navigation
feat(eql): one-parameter jsonpath predicates on json_tree documents (json_tree_match, @@, match) - #1446
Conversation
🦋 Changeset detectedLatest commit: c6da7e2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 227 | 0 | 51 | 0 |
Every mutant in the changed lines was caught by a test.
94db05b to
6a3419d
Compare
|
Force-pushed after the mutation run: the stack-encrypt commit now also carries the parser tests that kill the 15 mutants the first run missed (nesting depth, a second filter, |
| at | ||
| }, | ||
| ); | ||
| if full.len() > MAX_DEPTH { |
There was a problem hiding this comment.
A single-equality filter whose base + member path exceeds MAX_DEPTH returns term(TermError::JsonTooDeep) (an Error::Cipher), while the PR/module docs say a path deeper than a document is Error::Predicate { reason: JsonPredicateTooDeep, at }. member_of has the same shape for the multi-condition filter. A client mirroring the refusals (#1443) gets a different error type with no offset for this one case; use the Refusal { JsonPredicateTooDeep, C_NESTING, at: path.at } form as steps() does.
There was a problem hiding this comment.
Fixed in 6a31ad7.
- The compiler checks the depth of base + member for every condition of a filter, before anything is derived.
- A filter's joined path deeper than a document is now
Error::Predicate { reason: JsonPredicateTooDeep, construct: "nesting", at: <the filter's path> }. This holds for the single-equality filter and the multi-condition (element_match) filter alike, so neither surfacesTermError::JsonTooDeepany more. - Tests cover both forms, and the path exactly as deep as a document still compiles.
6a3419d to
6a31ad7
Compare
8c14324 to
15e9c56
Compare
6a31ad7 to
500aa58
Compare
|
Rebased onto #1445's new head (15e9c56); new head 500aa58. Why: #1444 and #1445 changed under review (#1444's tests moved to real Stack Encrypt documents and gained upgrade note U-004; #1445 got the same test move and the clippy fix that gates This PR's SQLx file |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation: automated code review (medium) plus cross-check against existing PR feedback, at head 500aa581.
Result: no critical or blocking issues. No new findings.
What was reviewed, read at head 500aa581:
packages/eql/src/v3/json_tree/predicate.sql, with thetypes.sqlandelements.sqlrefactors. I checked the three-valuedand/or/not, the short-circuit that keeps an unread element leaf from raising, thecmpleaf routed throughjson_tree_within_bounds, thecontainsrule againstjson_tree_contains, and the@@overload.- The compiler half of
packages/stack-encrypt/src/dynamic/json_predicate.rs:comparison,filter,memberandsimplify. The!=and ordering guards, thenot(not x)and dedupe rewrites, and the depth checks preserve the stated answers. - The
eql-bindings,TargetDescriptorand Go-guest wiring diffs.
Cross-check:
- The 7 existing review threads (doctests and docs, guest wiring, the
JsonTooDeeprefusal, keyword case, lax wrapping, the per-row@?walk,json_tree_within_boundsreuse) have all been answered with a fix or a stated deferral, and the head still matches those replies. Nothing is re-raised here. - The changeset covers
@cipherstash/eql. Noskills/*file mentions the json_tree operators, so no skill drift.
Not verified: I did not run the Rust or PostgreSQL suites, and read the ~1.8k-line parser and the test files only selectively. The PR description's results and the cargo-mutants report are the evidence for those.
Non-blocking observation: json_tree_is_valid_predicate_payload recurses without a depth bound. The engine caps nesting at 32, but a hand-built operand nested thousands deep would fail with a stack-depth error (54001) rather than a CHECK violation. It fails loudly, so this is optional hardening.
Generated by Claude Code
500aa58 to
6109e38
Compare
|
New head: 6109e38. The stack-encrypt commit now refuses a path step past |
6109e38 to
8b8cfc6
Compare
15e9c56 to
58fb199
Compare
58fb199 to
3e0fe7b
Compare
8b8cfc6 to
38510d8
Compare
|
The typed Rust interface for the operator this PR adds is #1459: today it is reachable only through |
38510d8 to
3dfbe7f
Compare
A filter compiler that targets `jsonb_path_match(col, $1, $2)` binds one predicate and its values. On `public.eql_v4_json_search` each question is its own operator, so the same filter had to be assembled per leaf. `eql_v4.query_json_predicate` is a boolean tree, one key per node (`and`, `or`, `not`, `contains`, `exists`, `cmp`, `elements`), whose leaves are the operands the family's operators already take. Its CHECK validates each leaf with that operand's own validator: the needle's, a new `json_tree_is_valid_bounds` that the element member's CHECK now calls too, and the elements predicate's. `eql_v4.json_tree_match` evaluates the tree with SQL three-valued logic, so it answers what the hand-written WHERE of those operators answers, NULL included, and `@@` on `(eql_v4_json_search, eql_v4.query_json_predicate)` is the operator form. The jsonpath `@@` blocker stays; an untyped operand now matches both and is refused as ambiguous rather than reaching native jsonb. The document is decompressed once per row and indexed once when the tree has an element leaf: the element match body moves into `json_tree_element_match_opened`, which both functions call. A `contains` leaf applies `json_tree_contains`'s rule to the projection read once. Children are read in order and the read stops at the first that decides the answer, so an element leaf the answer does not depend on cannot raise. The domain joins the catalog as the json_tree family's PredicateQuery role, so the eql-bindings v4 inventory and the Go type `eql.JSONSearchPredicateQuery` come from the generator. Refs #1441 Claude-Session: https://claude.ai/code/session_01MrxG724qYKcAWJdcABBFNY
…ent operand
`match` takes what `jsonb_path_match` takes, `{"predicate", "vars"?,
"wildcard_bound"?}`, and derives the `eql_v4.query_json_predicate`
operand. A hand-written parser reads the subset of SQL/JSON path
predicates the terms can answer: comparisons of a path (`$.a.b`,
`$.a[*].b`, `$."a b"`) with a variable or a literal, `exists(path)`,
`exists(path ? (@.x == $v && @.y > $w))`, `&&`, `||`, `!` and
parentheses. Everything else is refused at the byte it starts, as
`Error::Predicate`, naming the construct in the grammar's words and
never quoting the text, whose keys and values can be customer data.
Each comparison is compiled onto an existing operator's input and
derived by that operator's code: `==` to `eq_at`'s needle, `!=` to a
`selector` and not the needle, an ordering to a `selector` and the
ordering's leaf, so an absent path answers false as jsonpath has it; an
ordering at a wildcard and
a filter of more than one condition to `element_match`, a filter of one
equality to `eq_at`. No new derivation. `!=` at a wildcard, which asks
whether some element differs, is refused.
An ordering under an odd number of `!` also asks that its node is not a
JSON null, the `null` needle, since there jsonpath's false of a null and
the ordering's NULL pick different rows. Under no `!`, or an even number,
`and` and `or` cannot turn a child's NULL into true, so the needle would
change no row and only tell the database which rows hold null at the
path; it is not sent, and an ordering of a null answers NULL there.
New reasons: `json_predicate_syntax`, `json_predicate_unsupported`,
`json_predicate_unknown_variable`, `json_predicate_value_type`,
`json_predicate_not_equal_at_wildcard` and `json_predicate_too_deep`.
The operator is listed only on a type that names the domain
(`TargetDescriptor::with_predicate_query_sql_domain`), so a host that
does not, such as the Go guest today, sees no new operator.
Refs #1441
Claude-Session: https://claude.ai/code/session_01MrxG73fnX1p7oiizh3GRG5
…stgreSQL Documents sealed by Stack Encrypt's JSON index, predicates compiled by `match` through a data plan over eql-bindings' target table, and `eql_v4.json_tree_match` and `@@` over the column against PostgreSQL's `jsonb_path_match` over the plaintexts: the same rows, and the same answer row by row save NULL where jsonpath says false of an ordering over a null that sends no `null` needle. And against the WHERE written from the operands each leaf's own operator derives, NULL included. The rest of the SQL's contract is pinned over the same documents and derived operands: the domain's CHECK node kind by node kind, over a compiled tree and its parts; for every predicate, the WHERE written from the compiled tree's own leaves; three-valued logic under `and`, `or` and `not`, and an ordering leaf alone; a `contains` leaf as `@>`, the root scalar's `alt` included; an `elements` leaf as the element match, a leaf the answer does not depend on left unread; and `@@` with a typed predicate, a blocked jsonpath, an untyped operand and NULLs. EQL tests run over real encrypted data, and the SQLx suite has no way to produce a JSON tree document, so these live here. Refs #1441 Claude-Session: https://claude.ai/code/session_01MrxG724qYKcAWJdcABBFNY
3dfbe7f to
c6da7e2
Compare
3e0fe7b to
b7fa5a6
Compare
Summary
EQL v4 stores an encrypted JSON document in a
public.eql_v4_json_searchcolumn. On a plaintextjsonbcolumn, an application that compiles a filter language into SQL (an AIP-160 filter engine, for example) emits one call,jsonb_path_match(col, $1, $2): a jsonpath predicate and its variables, two parameters. On the encrypted column the database cannot evaluate a path or a value, so each question is its own operator (@>,json_tree_path_exists,(col -> $sel) < $leaf,json_tree_element_match), and the same filter had to be assembled by hand, one parameter per leaf.This PR gives the encrypted column a one-parameter form again. Stack Encrypt's new query operator
matchtakes{"predicate", "vars"}, the same two thingsjsonb_path_matchtakes, parses the jsonpath subset the encrypted terms can answer, and compiles it into one operand: a boolean tree whose leaves are the operands the existing operators already take. The new SQL functioneql_v4.json_tree_match(col, $1::eql_v4.query_json_predicate), orcol @@ $1::eql_v4.query_json_predicate, evaluates the tree. Over the same documents it returns the rowsjsonb_path_matchreturns over the plaintexts, and answers as it does row by row, apart from five documented cases (one of which, a NULL for false, never changes the rows). No stored format changes.The trade-off is in the plan: the tree is evaluated inside a function, so
json_tree_matchfilters a scan and engages no index. The per-operator form stays the fast path.Changes
EQL (
packages/eql)src/v3/json_tree/predicate.sql(new, v4 only):eql_v4.query_json_predicateand its recursive CHECK. Each node is an object with one key:and,or,not,contains(a needle),exists(a selector),cmp(one or two ordering bounds at one selector) orelements(an element-scoped predicate). Each leaf is validated by its own domain's validator: the needle's, the element predicate's, and a new shared bounds validator;eql_v4.json_tree_match(e, p) RETURNS boolean, which follows SQL three-valued logic. Each leaf answers as its operator does on the column;@@overload on(public.eql_v4_json_search, eql_v4.query_json_predicate). Thejsonpathblocker stays.src/v3/json_tree/types.sql:eql_v4_internal.json_tree_is_valid_bounds. The element member's CHECK now calls it too, so one function defines the bounds shape.src/v3/json_tree/elements.sql:json_tree_element_matchmoved intoeql_v4_internal.json_tree_element_match_opened(iv, sv, tags, p). Both functions call it, so a tree with several element leaves opens and indexes the row once.eql_v4_internal.json_tree_within_bounds(entry, bounds), an inlinable SQL function that tests an entry againstgt/gte/lt/lte. An element member and acmpleaf both call it, so one function defines how bounds are evaluated.json_tree_element_match's answers are unchanged, and its tests pass as before.eql_domains::JSON_TREEgains thePredicateQueryrole (JsonSearchPredicateQuery,eql_v4.query_json_predicate). Generated from it: the eql-bindings v4 inventory, and the Go typeeql.JSONSearchPredicateQueryinlanguages/golang/encrypt/eql/eql_gen.go. Hand-written:JsonTreePredicateQueryandJsonTreeBoundsineql-bindings, andTarget::predicate_query_sql_domain().docs/reference/eql-functions.md: the node table with the per-operator equivalent of each node, the planner cost, the cast, and where the answer and jsonpath's differ. Also a leakage bullet injson-tree-leakage.md, and the file and domain inpackages/eql/AGENTS.md.@cipherstash/eqlminor (.changeset/eql-json-tree-match.md).Engine (
packages/stack-encrypt)dynamic/json_predicate.rs(new): a hand-written recursive-descent parser and a compiler. No new dependency.dynamic/json_query.rs:matchin the operator table, and a "Compiled predicates" module section (grammar, mapping, deviations, refusals).deriveis split into reading andderive_input, so every leaf is derived by its own operator's existing code.dynamic/query.rs,target.rs,mod.rs:OpSpec::Match(wire namematch, inputpredicate, fidelityexact),TargetDescriptor::with_predicate_query_sql_domain, a new error variantError::Predicate { field, reason, construct, at }, and six new reasons.json/path.rs:JsonPath::of(segments)(crate-private).The grammar and the compiled shape
Input:
{"predicate": "<jsonpath>", "vars": "<JSON text of an object>"?, "wildcard_bound": N?}.varsis the third argument ofjsonb_path_match, verbatim, so a compiler passes the same two values it passes today.Accepted, as PostgreSQL reads them:
$,.key,."quoted key"(JSON escapes),[*];$v,$"v") or a JSON literal (a string, a number,true,false,null);==,!=(or<>),<,<=,>,>=between a path and a value, either side first;exists(path), andexists(path ? (cond && …)), each condition a comparison on@or@.member;&&,||,!(…)and parentheses.How each comparison compiles:
P == v,P == nullcontains:eq_at's needleP != v,P != nulland[exists(P), not(contains(P:v))]P < v…P >= vand[exists(P), not(contains(P:null)), cmp(P < v)]P < vwith a wildcard inPelements:element_matchon the array throughP's last[*]exists(P)exists: the selectorexists(P ? (@.m == v))containsatP.m(exact)exists(P ? (@.m == v && @.n > w))elementsThe guards make an absent path and a JSON
nullanswer as jsonpath answers them. Without them,!($.age < 3)on a row with noagewould be NULL, where jsonpath says true.Refusals #1443 must mirror. Each is reported as
Error::Predicate, whose payload isreason,construct(a fixed string from the grammar, never text from the predicate) andat(the byte offset in the caller's predicate text):json_predicate_syntax: text the grammar does not read;json_predicate_unsupported: arithmetic,like_regex,starts with,is unknown, methods,strict/lax,.*,.**, slices,last, a comparison of two paths or two values, a filter outsideexists, and||,!,!=orexistsinside a filter;json_predicate_unknown_variable;json_predicate_value_type: an object or array anywhere; under an ordering, or in a filter of more than one condition, anything but a number or a string;json_predicate_not_equal_at_wildcard;json_predicate_too_deep: more than 32 levels of nesting, or a path deeper than a document;json_path_position([2]),json_path_no_wildcard(a filter on a path with no[*]),json_path_wildcard(a[*]in a filter's member).Unchanged, from the leaves' own operators:
TermError::JsonPathNotOrdered, for a range on a path the column does not order, andjson_wildcard_bound_zero,json_inputandjson_syntaxfor the input object itself.Verification
All of this ran locally on macOS, against a private PostgreSQL 17 container on port 7641, after
mise run buildinpackages/eql.Engine (
packages/stack-encrypt)mise x --env test -- cargo nextest run -p stack-encrypt --all-features: 1028 passed. New tests:dynamic/json_predicate.rs. They cover each accepted form and what it compiles to, and every refusal by reason, construct and byte offset (about 90 cases). One test checks that a refusal never quotes text from the predicate, and another that a compiled tree'sDebugshows only its operators.tests/json_operators.rs(compiled_match):json_tree_matchover sealed documents, compared by a 64-case proptest with an independent jsonpath oracle over the plaintexts (three-valued, over absent fields, nulls, wildcards and filters);check_query, with the field and the offset;operators().match, with a marked literal and a marked variable.cargo clippy --workspace --all-targets -D warningsunder default features and--all-features,mise run wasm:wasi-check,mise run wasm:no-http-test,cargo fmt --all --check, doctests (91 across the workspace, including a new example onOpSpec::Matchthat runs predicates throughcheck_query),mise run doc:stack-encrypt, and stack-encrypt nextest--all-features(1028 passed): clean.cargo mutants -p stack-encrypt --in-diff <diff against #1445's head>: 274 mutants, 222 caught, 51 unviable, 1 timeout, 0 missed.lastand mode-word operands, a quoted key after., number literals with a fraction or a signed exponent, the depth limit on a path and on a filter's joined path, andNode'sDebug.+=replaced by-=in the parser's cursor. That loops forever, which the run stops.Default::default()replacements of types that have noDefault.--in-diffagainst the previous head, 6a3419d): 14 caught, 5 unviable, 0 missed.--in-diffrun was stopped part way on the owner's call to leave mutants to CI: 118 of 278 tested, 100 caught, 18 unviable, 0 missed.EQL (
packages/eql)SQLx
lint_tests(12),build_validation_tests(5),selector_literal_guard(10) andv3_public_surface_tests(7): passed.mise run test:encryption:postgreson a fresh database: all suites passed.json_treeis 36 tests, 8 of them new here, every one over real Stack Encrypt documents and predicates compiled bymatchthrough eql-bindings' target table. EQL tests must run over real encrypted data and the SQLx suite has no way to produce a JSON tree document, so the SQL's contract, which a SQLx file with a SQL stand-in for the JSON index pinned before, is pinned here. Six of the new tests cover:json_tree_matchequals the WHERE written from the compiled tree's own leaves (@>,json_tree_path_exists,->with the orderings,json_tree_element_match),@@equalsjson_tree_match, and all are true of exactlyjsonb_path_match's rows over the plaintext, answering as it does row by row save NULL for false where an ordering over anullsends nonullneedle;and,orandnot, and an ordering leaf alone (NULL where the path is absent ornull);containsequals@>, including the root scalar'salt;elementsleaf equalsjson_tree_element_match; a leaf the answer does not depend on is not read and does not raise, one it does depend on raises22023;@@with a typed operand or a bound parameter works,@@ '…'::jsonpathstill raises, an untyped literal is refused with42725, and a NULL argument gives NULL.The other two:
!,||, and!=/== null/absent cases):json_tree_matchand@@are true of exactlyjsonb_path_match(plain, predicate, vars)'s rows, with the same answers save that NULL for false;json_tree_path_exists,@>,->with>=, andjson_tree_element_match), and also equalsjsonb_path_match;mise run test:crates(498 tests),test:encryption(543),types:check,typescript:checkandcodegen:parity: passed.docs:validate:required-tags,docs:validate:coverage,docs:validate:documented-sql(against my own container),test:doc-anchorsandtest:docs_v3_grep: passed.The v3 and v4 bundles install side by side, and
eql_v3.lints()andeql_v4.lints()both return 0 rows. The v3 bundle contains none of the new SQL.Go
mise run wasm:guest:test: passed, including the new guest test above.wasm:guest:build,wasm:auth-guest:build,wasm:guest:build:deterministic,wasm:guest:build:eqlandwasm:guest:build:eql:deterministic. Thenmise run go:testpassed for every package.Not run
v3_v4_coexistence_testsdoes not compile here. It reads the fixtures that need CipherStash credentials, and this environment has none (as on feat(eql): test equality and ranges on one array element of a json_tree document #1445). I checked its lint assertion by hand, as above.pnpm run code:check: no JS or TS changed.Related
Closes #1441
Stacked on #1445 (which is on #1444). Refs #1443 (the Go
Matchcall on top).Review notes
The answer matches jsonpath except in five places. Four come from the stored document or the ordering terms, not from the predicate; the fifth (a NULL where jsonpath says false) never changes the rows. All five are documented in
eql-functions.mdand the module docs:A path is read as written, so lax mode's array unwrapping (an array where the predicate has no
[*]) is not applied.Lax mode's wrapping is not applied either. Where the predicate names
[*]and the document holds a value that is not an array, jsonpath wraps it, and the tree finds nothing. For example,$.tags[*] == "a"over{"tags": "a"}is true to jsonpath and false here, because$.tags[*]and$.tagshave different selectors.A comparison of a string with a number is unknown (NULL) to jsonpath, but the terms do not record a value's kind. So it is false under
==, true under!=, and decided by the terms under an ordering.Orderings, and filters with more than one condition (which compile to
element_match), compare the folded ordering terms.An ordering of a JSON
nullis NULL rather than false, except under an odd number of!. That picks the same rows, so aWHEREis unaffected; aSELECTof the boolean can show NULL. See the next note.The parity tests use documents where the first four do not apply. They require the same rows as
jsonb_path_match, and the same answers save the fifth case's NULL for false (assert_jsonpath_rows).The
nullneedle is sent only under an odd number of!(owner decision). jsonpath answers false of an ordering over a JSONnull, where the ordering's term comparison is NULL. Only a!turns that difference into different rows:andandorare monotone, so a child's NULL cannot become true, and under an even number of!the rows are the same. So an ordering compiles toand(exists, cmp), and under an odd number of!toand(exists, not(contains null), cmp). The needle tells the database which rows holdnullat the path, so it now goes only where it changes an answer's rows.an_ordering_sends_the_null_needle_only_under_an_odd_number_of_negationspins the placement, including!!, a!over||, mixed depths, and element leaves (which never carry it). The proptest oracle (now 256 cases) requires jsonpath's rows for every random predicate and document, and allows only NULL for false in the answer. The PostgreSQL parity tests check the same againstjsonb_path_match, with negated, doubly negated and mixed predicates added.Refusals for an undeclared path carry no offset. A range on a path the column does not order still fails with the existing
TermError::JsonPathNotOrdered, as the brief asked. That error carries neither the path text (the crate keeps key names out of errors) nor an offset. Grammar refusals carry an offset. If you want the index-level refusals to carry the offset too, that is a small follow-up: wrap them inError::Predicatewith the path's offset.TargetDescriptor::operators()now filters JSON operators by named domain. Without that, a type's own list showedmatcheven when the type named no predicate domain. The Go guest reads that list, and failed withunknown operator "match"in four Go tests.element_matchnever hit this, because it also needs an ordered path. Now the type list and the field list agree, and a test pins it. This is the one change outside the new operator.The untyped
@@operand. Before this PR,col @@ '…'with an untyped literal reached the jsonpath blocker and raised. Now it raises42725(operator is not unique), still loudly. The docs say to cast.json_tree_matchopens the row once. It decompresses the document once and readsqonce. It builds the tag index only when the tree has an element leaf, which it checks with one$.**.elementswalk of the predicate per row (the predicate is small). A flag in the operand would remove that walk, but would change the operand shape; I left the shape as it is. Acontainsleaf appliesjson_tree_contains's two-line rule to the projection it has already read. Callingjson_tree_containsitself would re-read the TOASTed column for every leaf. Wrapping the rule in a shared SQL helper would stop the planner inlining@>into the GIN-indexed form, because the argument is costly and used twice.What Go SDK: add
Match(ctx, cipher, predicate, vars)on a JsonSearch field, binding one jsonpath-style predicate as one parameter, on top of #1441 #1443 needs (the Go guest does not name the domain, so the Go SDK sees no new operator today):languages/golang/encrypt/guest/src/targets.rs:with_predicate_query_sql_domain(target.predicate_query_sql_domain()), andwith_elements_query_sql_domain(...)for Go SDK: no way to test an equality and a range on the same array element of a JsonSearch field — add an element-scopedMatchon top of #1440 #1442. A new guest test,a_document_names_the_leaf_domain_and_not_the_predicate_domains, pins that the guest names neither domain today, so it fails until Go SDK: no way to test an equality and a range on the same array element of a JsonSearch field — add an element-scopedMatchon top of #1440 #1442/Go SDK: addMatch(ctx, cipher, predicate, vars)on a JsonSearch field, binding one jsonpath-style predicate as one parameter, on top of #1441 #1443 update it alongside the Go side;"match"inparseOperatorName(encrypt/checker.go) and in stashgen'sjsonOperators;"predicate"inchecker.go's list of shapes;Error::Predicatein the guest'sstatus_for_dynamic. Its catch-all maps it toSTATUS_INTERNALuntil then;reason,constructandat.JsonSubtreeKey::below(from feat(eql): test equality and ranges on one array element of a json_tree document #1445) is now gated ondynamicthere, and the one helper this PR adds (JsonPath::of) is gated the same way, so clippy passes with default features and with--all-features.https://claude.ai/code/session_01MrxG724qYKcAWJdcABBFNY