Repository navigation
fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated - #25136
Conversation
…tocol's merged view Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…utage; rig reads the protocol Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…very kernel shape Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…ridge as by GET /meta/object Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…p-package-objects Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 678e84ab87abd237a624fc971c816db82ce4b59b && git checkout 678e84ab87abd237a624fc971c816db82ce4b59b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abc25a3536e2f00cc55ee6745320cb8f7209c459 c0fb1c715f485d6f58a6eaf5ec5363db054c97c4 && git checkout -B drift-repro abc25a3536e2f00cc55ee6745320cb8f7209c459 && git merge --no-ff c0fb1c715f485d6f58a6eaf5ec5363db054c97c4
node scripts/docs-audit/affected-docs.mjs --json abc25a3536e2f00cc55ee6745320cb8f7209c459
|
Contract reviewServed-tier: Inputs: card #25120 (body, claim 6322493221, report 6324844375, takeover claim 6327821572), PR #25136 (body, five-file list, net diff against ① Derived judgments
Verdict on the face: judgments 1 to 3 and 5 to 8 narrow or hold; judgment 4 widens one input class, so this head is not the narrowing its ② Semver level
③ Boundary flagsReport comment 6324844375:
What a PASS on the next head needs: judgment 4 closed (the gate rethrows Implemented-by: VERDICT: FAIL Generated by Claude Code |
…p-package-objects
…tself, so aggregate_records cannot serve it readExposureDeclaration read the protocol's OBJECT_PACKAGE_DISABLED refusal as "no declaration", on the premise that the data plane then refuses with that code. callData's aggregate arm asks the engine and never the protocol, so aggregate_records served a disabled package's object, one declared apiEnabled: false included. The refusal is now thrown past the gate's fail-open catch, so every arm answers the protocol's own 404 OBJECT_PACKAGE_DISABLED. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…ts to REST's 404 OBJECT_PACKAGE_DISABLED Both shapes, both objects (the one declared apiEnabled: false and the exposed one), all six data verbs: refused at the gate with the store never asked. REST's answer for the same objects is read off a RestServer served over the same kernel's protocol. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…al on every data verb Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…ckage as REST's enforceApiAccess does Measured: with the refusal thrown at every ask, the dispatcher's /data door answered a body that is not a record 404 OBJECT_PACKAGE_DISABLED on a disabled package's object, where RestServer answers 400 VALIDATION_FAILED (its enforceApiAccess reads the refusal as "no declaration" and its request gate answers first). The early ask, the door's twin of enforceApiAccess, now defers that refusal to callData, which refuses it on every arm. Pinned: the door answers each request what REST answers. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…rmed body first, as on REST Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #25120 (body; claim 6322493221; report 6324844375; takeover claim 6327821572; rework note 6328222949; report 6329438310), PR #25136 (body, five-file list, the two PR comments including review of record 6328012432, FAIL on ① Derived judgments
Verdict on the face: every change narrows or holds; the one widening the prior record found is closed on every arm and every door; no public surface moves. This head is the narrowing its ② Semver level
③ Boundary flagsReport 6329438310:
Prior record ③ 4, card acceptance item 3 (the cloud walk): the seat ruled in rework note 6328222949 to keep New from this review, non-blocking, to carry:
What a merge needs beyond this record: the five required contexts are already Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Generated by Claude Code |
|
Stuck check on the accepted head, which is why this has not landed. Reading at 2026-10-22T07:05Z, cloud-repo session
|
|
Landing on the maintainer's word ·
Generated by Claude Code |
Fixes #25120
Clause-②: no (narrowing)
Metadata-protocol: none
The
/mcpHTTP bridge now reads objects from the protocol's merged view, the read REST serves.list_objectsanddescribe_objectsee every objectGET /api/v1/meta/objectlists to the same caller. The API-exposure gate the data verbs ask refuses a package object declaredapiEnabled: false, and refuses every object of a disabled package404 OBJECT_PACKAGE_DISABLEDon every verb,aggregate_recordsincluded, as REST refuses it. Nothing is copied into themetadataservice: a second copy would be a second thing to keep in sync.This round: the review of record on
d19b65bf1cThe contract-tier review of record (comment 6328012432, VERDICT FAIL) found one widening. The gate read the protocol's
OBJECT_PACKAGE_DISABLEDrefusal as "no declaration", on the premise that the data plane then refuses with that code. Five ofcallData's six arms reach the protocol, whose registry gate refuses it. Theaggregatearm does not: it asks the engine, and the engine serves a disabled package's objects. So ond19b65bf1c,aggregate_recordsserved a disabled package's object declaredapiEnabled: falsewherever themetadataservice held a copy, where the copy had refused it404 OBJECT_API_DISABLED. Contract item 6 and the changeset's "Also changed" sentence were false for that verb.Closed here.
callData's gate refuses a disabled package's object itself, with the protocol's own error, so every arm answers404 OBJECT_PACKAGE_DISABLED. The same rows also close a hole that predates this PR (review boundary flag ③ 5):aggregate_recordsserved a disabled package's exposed object on every kernel, before this PR and after it. Both are pinned below.REST's answer was measured on the same kernels, through a
RestServerserved over each kernel's protocol. It refuses both objects404 OBJECT_PACKAGE_DISABLEDon every well-formed/datarecord request. It answers a body that is not a record400 VALIDATION_FAILEDfirst, becauseenforceApiAccessreads the refusal as "no declaration" and its request gate runs next. The dispatcher's/datadoor asks this gate early too, as the twin ofenforceApiAccess. With the refusal thrown at that early ask, the door answered those three malformed bodies404 OBJECT_PACKAGE_DISABLED, a new divergence from REST (measured on both kernel shapes, both objects). So the early ask keeps REST's reading,callDatarefuses, and the door now answers every request what REST answers. That is pinned too.The defect, measured on base
4e2c42f363buildMcpBridgeansweredlist_objectsanddescribe_objectfrom themetadataservice (listObjects()/getObject()). The exposure gate every runtime data door asks (refuseUnexposedDataActioninpackages/runtime/src/action-execution.ts, called bycallData) read the same service and treated a missing copy as "no declaration", that is, allowed. That service holds a copy of an object only where something put one there. On a kernel with anenvironmentId, the full boot bridge is off. That coverscreateStandaloneStack(os devasenv_local,os serve,os start) and the cloud single-environment runtime. So an object that a package or a plugin registers in theSchemaRegistryduring the boot gets no copy.Real kernels (
ObjectKernel+MetadataPlugin+ObjectQLPluginover better-sqlite3:memory:). TheenvironmentIdshape usescreateStandaloneStack's plugin options verbatim. The fixture package has two objects, one declaredapiEnabled: false, registered through the kernel'smanifestservice:metadata.getObjectquerycreateenv_local, during the bootenv_local, installed after the boot (source: 'marketplace')environmentId, during the bootenvironmentId, installed after the bootThe first row is the card's live reading in miniature: a short list presented as a complete
totalCount, "not found" for every package object, andquery_recordsserved. The other three rows are controls: the boot bridge and the late-manifest bridge already copy, so they were right and must stay right.The gate half is real. The same object declared
apiEnabled: falsewas served byquery_records, andcreate_recordgot past the gate to the driver. The runtime dispatcher's/datadoor shares the gate, and on that shape it answeredGET /data/qa_pkg_gatedwith 200 andPOSTreached the driver.RestServerrefused the same object, because its gate already reads the protocol's by-name read.The multi-tenant (
environmentIdkernel) path. The merged read is per environment there. The bridge resolvesprotocolthroughdeps.resolveService(context, 'protocol', envId), the same scoped seammetadataand the skill read use. Each environment kernel'sgetMetaItemsreads its ownObjectQLregistry (readFlattenedMetaItems: "the SchemaRegistry is owned by THIS kernel's ObjectQL instance"). Measured: twoenvironmentIdkernels behind one host each list and gate their own package and none of the other's. That run is red with the fix ablated, because both lists come back empty.The stdio half measures the same defect. That transport is held off by the dispatch, so this is reported and not widened. Same fixture,
env_local, during the boot, driven through the realMCPServerPluginstdio session:list_objectsgave{"objects": [], "totalCount": 0},describe_objectgaveObject "qa_pkg_open" not found, andquery_recordson theapiEnabled: falseobject was served (total: 0). The control without anenvironmentIdlists, describes and refusesOBJECT_API_DISABLED.What changed
packages/runtime/src/domains/mcp.ts.list_objects/listObjectsDiagnosedanddescribe_objectreadgetMetaItems({ type: 'object' })through the existingMcpMergedMetadataReadseam. This is the shape commit e783e16 gave the skill read. The unwrap moved into one helper (readMergedItems) shared by the skill read and the object reads, and the skill read's behaviour is unchanged.describe_objectanswers from the same listing, so every listed object can be described. The three outcomes are the skill read's. With no merged read on the host, the code reads themetadataservice as before. When the merged read answers, it is served. When the merged read throws, the error goes to the client and nothing falls back.listObjectsDiagnosedkeeps askinglistDiagnosed('object')for thepartialverdict.packages/runtime/src/action-execution.ts. The exposure gate reads the object by name through the protocol, the readRestServer's own gate makes (enforceApiAccess). It uses the execution face (getMetaItemForExecution) where the protocol has one, andgetMetaItemotherwise. A host whose protocol has no by-name read keeps reading themetadataservice. A merged read that throws falls back to themetadataservice's copy, the source this gate read before: an outage must not widen exposure, and the caller's fail-opencatchis unchanged.OBJECT_PACKAGE_DISABLEDis a refusal, never a read failure.readExposureDeclarationthrows it (isObjectPackageDisabledRefusal), andrefuseUnexposedDataActionanswers it by call site (disabledPackage).callDataasks with'refuse'. The protocol's own error (code,status404,object) is thrown past the fail-opencatch, on every arm, theaggregatearm included./datadoor asks early, before its request gate, with the default'defer'. That is "no declaration", asenforceApiAccessreads it, so a malformed body is answered by the request gate first, as on REST. A well-formed request is refused when it reachescallData.packages/runtime/src/domains/data-write-request-gate.test.ts, a fixture re-judged. Its dispatcher door used to model the gate's old source (ametadatafake answered from the registry). The rig's protocol cannot read without a metadata store, so with this change that door passed only through the outage fallback. The registry-backed by-name stub now lives inbootStore(stubObjectReads) and serves both doors, and the dispatcher door is given nometadataservice. A gate that stopped reading the protocol reds 12 of its rows (measured ond19b65bf1c). 73 of 73 rows are green with the fix..changeset/25120-mcp-package-objects.md. Apatchfor@objectstack/runtime, with the BREAKING banner and the ADR-0087 dispositionnot-required (no-migration-prescription). Both declaration lines are in the body. Its "Also changed" sentence now states the disabled-package refusal on every verb.Contract
Every input whose answer changes, old to new:
list_objects(POST /api/v1/mcp) on a kernel with anenvironmentId, for an object a package or plugin registered during the boot. Old: omitted, withtotalCountshort and nopartial. New: listed. The non-sys_set equals whatGET /api/v1/meta/objectserves the same caller.describe_objectandvalidate_expressionon such an object. Old:Object "NAME" not found. New: described (fields, label,enableFeatures).query_records,get_record,aggregate_records,create_record,update_record,delete_recordon such an object declaredenable.apiEnabled: false. Old: served, and the writes reached the store. New:404 OBJECT_API_DISABLED, and the store is never asked. With anapiMethodswhitelist, a verb it leaves out gets405 OBJECT_API_METHOD_NOT_ALLOWEDwithallowed.The same declaration on every other door that asks this gate (the runtime dispatcher's
/datadoor, the@objectstack/honocatch-all;callDataunder business actions and declarative endpoints). Old: served. New: the same refusals as item 3, the codesRestServeranswers.A disabled package's objects, where the
metadataservice still holds a copy. For example, a kernel without anenvironmentId. Old:list_objectslisted them anddescribe_objectdescribed them, while REST listed neither. New: omitted and not found, as REST answers.Each of the six MCP data verbs on an object of a disabled package, whatever the object declares. New, on every kernel shape:
404 OBJECT_PACKAGE_DISABLED, the protocol's own error, refused at the gate with the store never asked. That is the code and statusRestServeranswers for the same object on every/datarecord route. Old:apiEnabled: false, with ametadatacopy (a kernel without anenvironmentId; a marketplace install):404 OBJECT_API_DISABLEDon all six verbs;apiEnabled: false, with no copy (anenvironmentIdkernel, registered during the boot):404 OBJECT_PACKAGE_DISABLEDon the five verbs that reach the protocol, andaggregate_recordsserved;404 OBJECT_PACKAGE_DISABLEDon the five, andaggregate_recordsserved. This hole predates this PR: theaggregatearm never asked the protocol.No refusal's status moves, and
aggregate_recordsstops serving.The dispatcher's
/datadoor on an object of a disabled package. A well-formed request:404 OBJECT_PACKAGE_DISABLED, as REST answers. Old: the same for an exposed object;404 OBJECT_API_DISABLEDfor one declaredapiEnabled: falsewith a copy. A body that is not a record (POST /data/:objectorPATCH /data/:object/:idwith an array,POST /data/:object/querywith{ "limit": "ten" }):400 VALIDATION_FAILED, as REST answers. Old: the same for an exposed object;404 OBJECT_API_DISABLEDfor one declaredapiEnabled: falsewith a copy. Nothing is served either way. Only which refusal comes first moves, to REST's order.An object whose served definition differs from the
metadataservice's copy (for example, a stored environment row layered over it). Old: MCP discovery and the gate read the copy. New: they read the definition REST serves.list_objects/describe_objectwhile the metadata store cannot be read. Old: themetadataservice's listing. New: the protocol's503 SERVICE_UNAVAILABLEerror envelope, which is whatGET /api/v1/meta/objectanswers and what the skill prompt read already answers.Unchanged: objects that already had a copy are listed and gated as before. That covers every object on a kernel without an
environmentId, a marketplace install on any kernel, and a runtime-authored object. A host with no merged read keeps reading themetadataservice. The gate's outage branch judges the copy, as before.packages/mcpis untouched.Pins
packages/runtime/src/domains/mcp-package-object-discovery.test.ts, 70 cases. The pins read the bridge the real/mcpentry (HttpDispatcher.handleMcp) hands the MCP service, solist_objectsrenderstotalCountfromlistObjectsDiagnosed().objects:list_objectslists the package's objects, its non-sys_set equalsGET /meta/object's for the same caller, and it is notpartial.describe_objectanswers both objects, includingapiEnabled: false. Each of the six data verbs refuses the gated object with{ code: 'OBJECT_API_DISABLED', status: 404 }, and no protocol data method or engineaggregateis called. Control:query_recordsserves the exposed object.404 OBJECT_PACKAGE_DISABLEDon each of its six well-formed/datarecord requests, read off aRestServerserved over the same kernel's protocol;/datadoor answers each of nine requests what REST answers: the six well-formed ones and three whose body is not a record;apiEnabled: falseand the exposed one, with{ code: 'OBJECT_PACKAGE_DISABLED', status: 404 }, and no protocol data method or engineaggregateis called.Red before, green after, on
e471998c5, whose code and pin blobsc0fb1c715carries unchanged (it changes only the changeset). Each leg's direction and count was predicted before running, and each landed as predicted. Mutations were landed and restored byscripts/ablation-replace.mjs(anchor 1 to 0, blob changed; restored blob equals HEAD,git diff HEADempty), under the verify lock. The whole-file leg ran under a script with anEXIT/INT/TERMtrap. It restored withgit checkout HEADand checked blob equality and an emptygit diff HEAD. The runtime's own source is read by relative import, so no leg depends on adist/build.abc25a353(blobs616639b765,5d19bd3b7a)env_localduring-boot list, describe and six verbs (8); four two-environment pins; disabled package without anenvironmentId: listing, door parity and twelve verb rows (14); disabled package onenv_local: twelve verb rows (12)readMergedObjectsanswersundefined(themetadataservice read)readExposureDeclarationalways reads the copyenv_localduring-boot six verbs; two two-environment gate pins; disabled package without anenvironmentId: twelve verb rows and door parity; disabled package onenv_local: twelve verb rowsOBJECT_PACKAGE_DISABLEDread as "no declaration" again (the reading ond19b65bf1c)aggregate_recordsrows answerSERVED; the other twenty reached the protocol before it refuseddisabledPackagedefaults to'refuse'(the refusal thrown at every ask)404 OBJECT_PACKAGE_DISABLEDand REST answers400 VALIDATION_FAILEDThe first attempt at the gate leg was a no-op. Its replacement text already occurred twice in the file, so
ablation-replace.mjsrefused it and ran nothing. It was re-run with a unique replacement, and the row above is that run. The rig leg (12 failed, 61 passed) was measured ond19b65bf1cand not re-run: the rig and its healthy path are unchanged.Local verification
On
c0fb1c715, the final commit, withorigin/mainabc25a353merged in and the workspace built (turbo run build --filter=!@objectstack/docs, 76 tasks, exit 0). Every heavy run went through the verify lock, and each line cites the lock'sVERDICTline.@objectstack/runtimeunit layer (vitest run --project local), every file in one run: 433 files passed; 6864 tests passed, 19 skipped, 0 failed. That is 29 more than ond19b65bf1c, the pin's 41 cases now 70.test:repois CI's: the diff touches none of its files, and the derivation does not name it.pnpm --filter @objectstack/runtime typecheck: exit 0 (tsc --noEmit, andcheck:test-typecheckOK: 27 files, 190 errors, 68 pinned signatures held in the ledger). Both edited test files are in thetsconfig.test.jsonprogram (--listFilesOnly). A first run, before the peer@objectstack/driver-tursowas built, reported TS2307 on an untouched file; that was build state, not this change.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 68 commands, each exit code captured before any pipe, all exit 0. Two first answered 3 (PREREQUISITE NOT MET).check:dual-build-cjs-loadshad no full build yet;check:type-check-debttimed out queueing for the lock behind another session's run. Both were re-run after the full build, exit 0. The 10 families declared WIDE-population were run too, all exit 0.--ranreconciliation:✓ dispatch-gates --ran: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3).pnpm lint(eslint . --no-inline-config): exit 0.node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, with PAYLOAD apull_requestevent carrying this body: exit 0.✓ This diff introduces no major bump.and✓ LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol none ....node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0,✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.node scripts/check-closing-keyword-parity.mjs --bodyon this body binds#25120alone.pnpm --filter @objectstack/dogfood test, no shard):VERDICT command-exit 0; Test Files 287 passed, 3 skipped (290); Tests 2395 passed, 18 skipped (2413); no failure.CI on
c0fb1c715: 41 check runs, 34successand 7skippedby design, 0 failures. The five required contexts aresuccess: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core and Governed Surface Queue Guard.Acceptance notes
objectstack-ai/cloud, outside this session. In this repo the shape is theenvironmentIdkernel, pinned above withcreateStandaloneStack's options. On that shape, a package registered during the boot is the one that had no copy. This repo's marketplace install and rehydrate register after the boot and were already copied. Whether the cloud runtime registers HotCRM during the boot was not walked. The card's reading (18crm_*and 8ai_*objects missing) fits that path.aggregatearm never enforcedOBJECT_PACKAGE_DISABLED(review boundary flag ③ 5). Before this PR,aggregate_recordsserved a disabled package's exposed object on every kernel, becausecallData'saggregatearm asks the engine, never the protocol. The gate's refusal closes it. Pinned by theaggregate_recordsrows on the exposed object, which the refusal leg turns toSERVED.RestServeranswers a body that is not a record400 VALIDATION_FAILEDon a disabled package's object, before its data plane's404 OBJECT_PACKAGE_DISABLED, becauseenforceApiAccessreads that refusal as "no declaration". For an object declaredapiEnabled: falseit is the other way round: the exposure refusal comes first. The dispatcher's door now follows REST. Whether both doors should refuse a disabled package's object before the shape verdict is REST's question, and nothing here changes it.RestServer'sloadObjectItemlogs a warn when a thrown by-name read makes it fail open. This gate does not fail open on that throw: it judges themetadataservice's copy, so REST's line ("failing open") would say something false here.ActionExecutionDepsalso carries no logger. Left silent, as the oldcatchwas.list_actions/run_action.collectActionDeclarationsreadsmetadata.listObjects(), and action resolution reads the same service. Measured on the sameenv_localduring-boot fixture: an object action declaredoperation: 'update'withai: { exposed: true }is in the registry, butlist_actionsanswers[]andrun_actionanswersAction 'mark_done' not found on object 'qa_pkg_open'. Control without anenvironmentId: listed and resolved. Queued as mcp: on an environmentId kernel, list_actions / run_action cannot see a package object's actions when the package registered during boot — collectActionDeclarations and action resolution read metadata.listObjects() #25139.packages/mcp/src/stdio-data-bridge.ts): the same three answers, measured above. Held off by the dispatch, queued as mcp stdio: on an environmentId kernel, a package registered during boot is invisible to list_objects / describe_object, and the stdio exposure gate serves its apiEnabled:false object (the stdio twin of #25120) #25138. Its fix should readOBJECT_PACKAGE_DISABLEDas this gate now does on the arm that does not reach the protocol.GATED_ACTIONSdocblock inpackages/mcp/src/stdio-data-bridge.tssays the HTTP bridge answerslistObjects/describeObject"straight off the metadata service". After this change, it reads the protocol's merged listing. The parity claim it makes (those two are not gated throughcallData) still holds. Only the wording is stale. It rides on mcp stdio: on an environmentId kernel, a package registered during boot is invisible to list_objects / describe_object, and the stdio exposure gate serves its apiEnabled:false object (the stdio twin of #25120) #25138.describe_objectserves the rawfieldsmap and reads the whole served listing per call.Generated by Claude Code