Repository navigation
Macro-generate capability registrations; harden runtime timeouts and network egress; add platform + eval CI - #5
Merged
Merged
Conversation
JavaScriptCore drains the entire promise graph inside a single
evaluateScript call because bridge calls are synchronous, so the
wall-clock poll loop after evaluation could never interrupt CPU-bound
scripts: while(true){} pinned a worker thread forever and timeoutMs
was never enforced against running JavaScript.
Install a JSContextGroupSetExecutionTimeLimit watchdog per execution
whose callback re-checks the deadline and the cancellation flag every
50ms and terminates the script when either trips. Termination is
classified as EXECUTION_TIMEOUT (or SEARCH_TIMEOUT for search) and
CANCELLED, so call.cancel() now interrupts in-flight JavaScript
instead of only flipping a flag that was polled after completion.
The deadline now starts when evaluation begins rather than after the
script settles, and the dangling-promise poll loop reuses the same
deadline instead of granting a second full timeout budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
network.fetch previously accepted any HTTP(S) destination once the capability was allowlisted, so agent-authored JavaScript could reach loopback services, RFC 1918 hosts, and cloud metadata endpoints such as 169.254.169.254, follow redirects from public URLs to internal ones, and buffer arbitrarily large response bodies into memory. Introduce NetworkAccessPolicy, injected via CodeModeConfiguration.networkAccessPolicy: - .standard (the default) refuses loopback, private-range, link-local, CGNAT, and unique-local IPv4/IPv6 destinations (including encoded literals like 2130706433 and 0x7f000001 via inet_aton, and IPv4-mapped IPv6), plus localhost and .local/.localhost/.internal names, and caps buffered responses at 10 MB. - allowedHosts/blockedHosts support exact and subdomain matching, with explicit allowlist entries able to deliberately re-enable private hosts. .permissive restores the previous unrestricted behavior. - The fetch transfer now runs through a per-task URLSession delegate so redirect targets are re-validated before being followed and oversized bodies are cancelled mid-transfer instead of buffered. - Refusals throw structured NETWORK_POLICY_VIOLATION errors with repair suggestions, and both denials and successful fetch destinations are written to the audit logger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
The install snippet told users to depend on from: "0.1.0", but the repository has no tags, so Swift Package Manager cannot resolve that requirement. Document the branch-based dependency as the working form until a release is tagged, and keep the versioned form as the preferred path once 0.1.0 exists. Also drop the deprecated name: parameter from the .package declaration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Compile blocker: JSContextGroupSetExecutionTimeLimit and
JSContextGroupClearExecutionTimeLimit are exported by JavaScriptCore
but declared only in a private WebKit header, so the watchdog would
not compile against the public SDK module. Route both through a new
CCodeModeJSC C shim target that re-declares the exported symbols, and
document the private-API dependency in the watchdog and README.
Correctness (execution):
- The post-evaluation poll loop used 'while Date() < deadline' as its
guard, so a script that had already fulfilled just as the deadline
passed skipped the loop entirely and threw a spurious timeout that
discarded a completed result. Replace both poll loops with a shared
waitForSettlement helper that checks the settled state before the
deadline on every iteration (and re-checks watchdog termination),
and returns an already-settled result even a hair past the deadline.
- Result serialization ran with the watchdog uninstalled, so a result
with a runaway getter/toJSON (e.g. { get x() { while(true){} } })
hung the execution thread forever. Keep the watchdog installed and
rearm it with a fresh bounded budget for the decode phase, so
legitimate serialization still completes while runaway getters are
terminated.
- Fold the duplicated execute/search termination + poll logic into the
shared helper.
Security (network policy):
- Host normalization now strips a trailing root dot, so fully-qualified
spellings like 'localhost.' or 'metadata.google.internal.' can no
longer bypass the loopback/metadata block or the allow/deny lists.
- IPv6 private detection now also covers IPv4-compatible (::a.b.c.d) and
NAT64 (64:ff9b::/96) embeddings of private/loopback IPv4 addresses.
Efficiency:
- Reserve the response buffer from a known Content-Length instead of
growing it through repeated reallocations, and build the success
audit/log summary string once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
swift test on macOS never compiled the UIKit presenters or the CCodeModeJSC watchdog shim against the iOS/visionOS SDKs, so a platform-specific break (like the private-header symbol the shim resolves) could land undetected. Add a platform-build matrix that runs xcodebuild for iOS and visionOS on every PR and push, plus SwiftPM build caching and toolchain-version logging on the deterministic job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
CLRegion / CLCircularRegion are unavailable on visionOS, so the region-scoped CLGeocoder.geocodeAddressString(_:in:) overload and the coreLocationRegion helper failed to compile there. The package declared .visionOS(.v2) support but was never actually built for visionOS until the new platform-build CI job exercised it. Use the unscoped geocodeAddressString overload on visionOS and compile the region helper out there; other platforms keep region-scoped geocoding. Pre-existing issue surfaced by the new CI matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
First macOS verification run of the Linux-authored watchdog hung the whole test suite: on macOS 26 JavaScriptCore invokes the termination callback exactly once per installed limit, and returning false permanently disarms the watchdog instead of re-arming it as documented. Every runaway script (while(true), promise-chain loops, runaway getters during serialization) therefore ran forever after the first 50ms check. Root-caused with a minimal C reproduction against JSContextGroupSet- ExecutionTimeLimit. The callback is now a self-referencing C function that re-installs the time limit before returning false, restoring periodic deadline/cancellation checks. All 8 ExecutionWatchdogTests now pass.
…an drift Bridges parse constrained string arguments case-insensitively (lowercased() throughout SystemUI/EventKit/Photos/FileSystem bridges), but the registry's allowedStringValues check was exact-match, so it rejected inputs the bridges accept. Validation now compares case-insensitively, the calendarDelete span list includes the alias spellings EventKitBridge accepts (this_event, future, ...), and the lowercase videoQuality duplicates that worked around the case sensitivity are dropped. Also adds a test pinning that no built-in registration gates on .healthKit via requiredPermissions: the default broker can never report .granted for HealthKit (read authorization is opaque by design), so such a registration would fail closed unconditionally. HealthBridge does per-type authorization itself.
Executions run on a background queue while the host may synchronously block the main thread waiting on the result; DispatchQueue.main.sync from the broker then deadlocks. Use main.async — the existing 10s delegate wait already bounds the request, and if main never runs it the wait times out and returns the current status instead of hanging.
PathPolicy: empty/whitespace paths, scoped roots, appGroup with and without a configured root, absolute paths inside/outside allowed roots, dot-dot escapes vs internal dot-dot, nonexistent nested paths, and symlinks that escape vs symlinks between allowed roots. Plus round-trip/uniqueness tests for InMemoryArtifactStore and ordering/drain/concurrency tests for SyncAuditLogger. SystemPermissionBroker is left untested deliberately: every path ends in a real OS framework call and needs injectable seams first (noted in TODO).
Audit of Tools/CodeModeAuthoring (builds and passes all 14 tests on the current toolchain) against the three built-in registration idioms and the parallel sources of truth behind the metadata drift. Proposes a three-phase path: converge built-ins on the typed-tool protocol with enum-typed constrained arguments (drift-proof by construction, no macro needed), then promote the macro into the core package as @BuiltInCodeMode generating the decode/metadata boilerplate, then migrate per domain and delete the central constraint and type-inference tables. Includes measured swift-syntax build cost (24s clean, ~0 incremental, prebuilts default-on) since that was the original reason the macro package was kept out of the core graph.
… enums Promotes BuiltInCodeModeTool out of SimpleBuiltInCodeModeProviders.swift as the standard registration idiom, extended with capability IDs, JS name aliases, and required permissions. Adds CodeModeStringEnum: a constrained string argument is declared once as an enum, and the advertised allowedStringValues, the case-insensitive/alias-aware decode, and the bridge parsing all derive from that declaration, so they cannot drift. Migrates the EventKit domain (9 registrations, 5 enums) to prove the shape: EventKitBridge and SystemUIBridge now parse span/operation/picker styles through the shared enums, and the four EventKit-domain rows are deleted from the central CapabilityArgumentConstraints.defaults table. Keychain, location, and weather tools converge on the same protocol; the raw-CodeModeRegistration and builtInCapability: glue idioms are gone. Adds constrainedArgumentMetadataIsCoherentForAllRegistrations (every constrained argument must be a declared argument) and reworks the span test to pin enum <-> descriptor <-> bridge agreement. Transitional tools carry a raw passthrough because bridges still consume raw dictionaries; the Phase 2 macro generates decode and the argument metadata, which is where the line-count win lands. 207 tests pass.
Folds Tools/CodeModeAuthoring into the root package: CodeModeMacros becomes a macro target of the main package (swift-syntax 601.0.1, prebuilts default-on; eval CLI cold build measured at 29s) and the host-facing @CodeMode surface ships as the CodeModeAuthoring product. The same compiler plugin now also provides the internal @BuiltInCodeMode + @ToolParam macros. @BuiltInCodeMode generates the mechanical members of a BuiltInCodeModeTool from its Arguments struct: identity statics, the codeModeArguments metadata list, and decode(arguments:). Curated prose, permissions, and call(arguments:context:) stay hand-written. Field types the macro does not recognize as JSON primitives are emitted through CodeModeStringEnum- constrained overloads, so the compiler enforces the semantics on the expansion and constraint metadata derives from the enum. A trailing un-annotated raw: [String: JSONValue] receives the canonicalized passthrough transitional tools need while bridges consume raw dictionaries. The EventKit domain is converted to the macro (its hand-written decode and argument lists are deleted). Adds five expansion tests (generated source + diagnostics), behavioral decode tests, and a metadata baseline test pinning the advertised surface of remindersWrite. CI xcodebuild jobs get -skipMacroValidation for the package-local plugin. 229 tests pass.
CapabilityMetadataGoldenTests pins every registration's full advertised surface (jsNames, prose, permissions, argument lists/types/hints, constraints, resultSummary) against a committed 115-capability JSON baseline; CODEMODE_REGENERATE_GOLDEN=1 regenerates it, and the JSON diff becomes the review artifact for registration refactors. This is the safety rail that makes the remaining domain migrations mechanically verifiable. PHASE3-HANDOFF.md is the self-contained work spec for converting the seven remaining CapabilityRegistrations files to @BuiltInCodeMode: per-registration recipe (including the effective-type rule for inferred/.any arguments and the constrained-enum rewiring pattern), domain order with per-domain notes, the allowed golden-diff categories, and hard rules (no prose rewording, no permission-ownership changes, skip-don't-improvise). 230 tests pass.
Converts all 12 interaction-UI capabilities (share, quicklook, camera capture/scan, mail, messages, print, web, auth, alert, prompt, settings) to macro-authored tools in SystemUICodeModeTools.swift; the registration file is now a thin list. Introduces 8 CodeModeStringEnums for the constrained values (MediaTypeFilter, CameraDevice, CameraFlashMode, CameraVideoQuality, DataScannerMode, DataScannerQualityLevel, PrintOutputType, AlertPreferredStyle) and rewires SystemUIBridge's own lowercased() validations to parse through them, making each enum the single source of truth. The cameraUICapture / cameraUIScanData / printUIPresent / uiAlertPresent rows are deleted from the central CapabilityArgumentConstraints.defaults table. AlertPreferredStyle keeps the all-lowercase 'actionsheet' spelling as an alias because SystemUIBridge accepts it (SystemUIBridge.swift validateAlertArguments); CameraVideoQuality's canonical spellings survive the presenters' lowercasing (UIKitSystemUIPresenter.videoQuality/printOutputType), so raw-passthrough canonicalization is behavior-preserving. Golden diff: one allowed category only — argumentHints gained title/message entries for uiPromptPresent (which advertised no hints for them before); values match the uiAlertPresent wording. 230 tests pass.
Converts the 10 filesystem capabilities (list, read, write, move, copy, delete, stat, mkdir, exists, access) to macro-authored tools in FileSystemCodeModeTools.swift; the filesystem section of +Core.swift is now a thin list. No constrained values, so no enums and no bridge rewiring. networkFetch is left on the flat-init idiom (PHASE3-SKIP comment): it declares nested dotted-path arguments (options.method, options.headers, …) that the flat @BuiltInCodeMode Arguments model cannot express without changing the advertised optionalArguments list, and its options.responseEncoding constraint stays in the central defaults table. Keychain already delegates to the Phase-1 converted builtins and is untouched. Golden diff: empty — every filesystem argument already had a hint, and types match the inferred baseline. 230 tests pass.
…eMode Converts all 13 capabilities (contacts read/search/pick/present/presentNew, photos read/export/pick/limited-picker, documents pick/export/openIn/scan) to macro-authored tools; both registration functions are now thin lists. Constrained values: photosRead and photosUIPick reuse MediaTypeFilter (from domain 1); contactsUIPick gets a new ContactPickerMode enum. Their three rows are deleted from the central defaults table. Rewires the owning parsers to the enums: PhotosBridge.read (mediaType -> MediaTypeFilter), SystemUIBridge validateContactPickerArguments (mode -> ContactPickerMode); photosUIPick's mediaType already validated through MediaTypeFilter via the shared validatePhotoPickerArguments rewired in domain 1. Also decouples constraintValidationMatchesCaseInsensitively from the removed contactsUIPick table row by constructing the constraints inline — it tests the generic validate() matching, which is unchanged. Golden diff: empty — advertised allowedStringValues are identical to the former table rows, all argument types match the inferred/declared baseline, and every argument already had a hint. 230 tests pass.
Converts all 19 vision/notifications/alarm/health/home/media capabilities to macro-authored tools in SystemServicesCodeModeTools.swift; the six registration functions are now thin lists. No constrained values in this domain, so no enums, no bridge rewiring, no table changes. Permission declarations are preserved exactly: notifications (.notifications), alarms (.alarmKit), home (.homeKit); vision/media declare none; and the three health tools declare no requiredPermissions — HealthKit read authorization is opaque so HealthBridge does its own per-type auth, and the noBuiltInRegistrationGatesOnHealthKitPermission invariant test still passes. homeWrite's value keeps its .any type (declared as JSONValue). Golden diff: empty — every argument already had a hint and types match the declared/inferred baseline. 230 tests pass.
…o @BuiltInCodeMode Converts all 15 capabilities in +CloudPushSpeech.swift (6 CloudKit, 5 remote notification, 4 speech) to macro-authored tools; the three sub-functions are now thin lists (the bigTicketAppleRegistrations aggregator is unchanged). The four database ops (queryRecords/saveRecord/deleteRecord/subscribe) share a new CloudKitDatabase enum (private/shared/public); its row is deleted from the central defaults table and SystemCloudKitMapping.databaseName/database now parse through the enum, so the advertised, decoded, and system-client-mapped values share one source. The mapping test (default private, shared, invalid archive) still passes. Golden diff: one allowed category — cloudKitSubscriptionSave gained containerIdentifier and zoneID hints (it advertised none for them); values are copied verbatim from the sibling CloudKit registrations. 230 tests pass.
…nCodeMode Converts all 18 capabilities in +IntentsModelsActivityMaps.swift to macro-authored tools; the four sub-functions are now thin lists. Constrained values: activityEnd gets an ActivityDismissalPolicy enum (default/immediate) and mapsRouteEstimate/mapsOpen share a MapsTransportType enum (automobile/walking/transit/any); both rows are deleted from the central defaults table. transportType parsing in SystemMapsMapping.transportTypeName is rewired through the enum (its allowedTransportTypes constant is removed); dismissalPolicy has no in-tree consumer (host ActivityClient handles it), so the enum gates it at the registry. The type-inference trap: foundationModelsGenerate.prompt is absent from the argument-type table, so its historical advertised type is 'any' — declared as JSONValue to preserve it, not String. Golden diff: one allowed category only — argumentHints gained entries for seven registrations that advertised no hints for those arguments (activityList, activityUpdate, activityEnd, activityPushTokenRead, mapsReverseGeocode, and the departureDate/arrivalDate and transportType gaps on the maps route/open tools). No types, constraints, or existing prose changed. 230 tests pass.
…main) Converts all 18 music/passKit/storeKit capabilities to macro-authored tools; the three sub-functions are now thin lists. Only musicPlaybackControl.action is constrained (a new MusicPlaybackAction enum: play/pause/stop/skipToNext/ skipToPrevious/playCatalog/playLibrary); its row is deleted from the central defaults table. Host clients stay authoritative for playback semantics, so no bridge rewiring — the enum only gates the advertised value set at the registry. The type-inference trap: musicPlaylistWrite.name is absent from the argument- type table, so its historical advertised type is 'any' — declared as JSONValue to preserve it, not String. Also makes registryValidationRejectsDescriptorConstrainedValuesBeforePermissions self-contained: it built a synthetic musicPlaybackControl descriptor that relied on the removed defaults(for:) row, so it now passes an explicit argumentConstraints and still verifies constraint-before-permission ordering. Golden diff: one allowed category only — argumentHints gained entries for five registrations that advertised none for those arguments (musicCatalogDetails countryCode, musicPlaybackControl startPlaying, musicPlaylistWrite description, passKitApplePayPresent timeoutMs, passKitPassPresent identifier+timeoutMs). This completes Phase 3: 114 of 115 capabilities are now macro-authored; networkFetch alone stays on the flat init (dotted-path arguments). Updates TODO.md and PLAN-registration-macros.md to reflect completion. 230 tests pass.
The scheduled (daily) run only fired 'codemode-eval plan', which is pure arithmetic (scenarios x repeat x maxTurns) printed to a log — no live model calls, no compare, no artifact, deterministic output. It burned a macOS runner daily for zero signal. Removed the 'schedule' trigger entirely. Deterministic evals + iOS/visionOS platform builds stay continuous: PRs and pushes to main (merges), unchanged. Replaces the no-op llm-plan job with a real llm-live regression gate, manual only (workflow_dispatch): it previews the budget, then — when a WAVELIKE_API_KEY secret is present and the CLI is built with the private overlay (real codemode-eval llm vs the LLMUnavailable stub) — runs the core/failures suites live and compares each against its committed baseline, failing on pass-rate or exact-capability regression. Absent those it skips the live steps with a warning rather than pretending. This encodes the intended pipeline so it goes fully live once the private Wavelike wiring + secret land, with no further workflow edits. Updates EVALS.md CI Policy to match.
zac
added a commit
that referenced
this pull request
Jul 26, 2026
The shipping default was `URLSession.shared`, which reads `HTTPCookieStorage.shared` and `URLCredentialStorage.shared`. Every script request therefore rode whatever the host app was already logged in to — authenticated session-riding against any origin with a live cookie — and a `Set-Cookie` in a script-fetched response poisoned the app's cookie jar. The tests all passed an `.ephemeral` session, so the shipping default was the one configuration never exercised. `network.fetch` now defaults to `NetworkBridge.isolatedSession`: ephemeral, with no cookie storage, no credential storage, no shared cache, and cookies disabled outright. Per-request cookie handling is disabled too, so a host that supplies its own session still does not attach its jar. Caller-supplied `Cookie`, `Authorization`, and `Proxy-*` headers were copied to the wire verbatim; they are now refused with `NETWORK_POLICY_VIOLATION` and audited. `NetworkAccessPolicy.allowsCredentialHeaders` re-enables them for hosts whose scripts legitimately call an authenticated API, and `.permissive` sets it, keeping that opt-out a faithful "old behavior" switch. Review finding P0 #5.
zac
added a commit
that referenced
this pull request
Oct 21, 2026
…#7) * Make the host, not the model, the capability boundary `allowedCapabilities`/`allowedCapabilityKeys` are model-authored tool arguments, and nothing in the package narrowed them, so a host that piped tool JSON straight into `executeJavaScript` — the README Quick Start shape — granted whatever the script declared for itself. Adds `CodeModeConfiguration.capabilityGrant` (`CapabilityGrant`), intersected with the request before the invocation context is built, so the effective set is always requested ∩ granted. Defaults to `.unrestricted` for source compatibility. Withheld identifiers produce a `CAPABILITY_WITHHELD_BY_HOST` diagnostic, and the resulting `CAPABILITY_DENIED` tells the model the denial is not repairable by widening its own allowlist, so it stops escalating. Also closes the aliasing hole: `allowedCapabilityKeys` accepts arbitrary strings and is not validated against `CapabilityID` at decode time, but `invokeBuiltIn` honoured it and `registeredFunction(for: key)` resolved built-ins first — so `{"allowedCapabilities": [], "allowedCapabilityKeys": ["keychain.read"]}` reached the built-in keychain bridge past any host vetting only the typed field. Built-ins are now gated by `allowedCapabilities` alone, the key namespace resolves custom providers only, and a key that spells a built-in is rejected on the provider path as defense in depth. Granting a built-in by its `CapabilityID` is unchanged. Review findings P0 #1 and #2. * Make adversarial number handling total instead of trapping `Int.init(_: Double)` traps on non-finite and out-of-range input, and every number reaching these paths is attacker-controlled: `JSONValue.intValue` backs 48 `.int(...)` call sites, and `JavaScriptExecutionRequest.decodeTimeoutMs` runs on model-authored tool JSON before any script executes. `{"timeoutMs": 1e300}` or `fetch(url, { timeoutMs: 1e300 })` killed the host process in-process. - `JSONValue.intValue` goes through a total `exactInt(from:)` that rejects NaN, infinity, and out-of-range magnitudes; in-range values truncate toward zero as before. - `decodeTimeoutMs` rejects doubles and numeric strings it cannot represent. - `timeoutMs` is clamped to the 1...60000 bounds the tool schema already advertises, in both the memberwise init and the decoder, so an over-large budget yields the ceiling rather than an unbounded run. - A declared-`.number` argument must now be finite: the type check rejects non-finite JSON numbers, and `coerceScalarString` no longer accepts `"inf"`/`"nan"`, which `Double.init(String)` parses happily. Review finding P0 #3. * Stop fs.move/fs.copy from recursively deleting their destination Both operations unconditionally recursive-deleted any existing destination, with no overwrite flag and no directory check, and containment alone admits a root: `DefaultPathPolicy.resolve(path: "documents:")` yields the documents root and `isAllowed` accepts it via `path == allowed`. So `apple.fs.move({ from: 'tmp:junk.txt', to: 'documents:' })` removed the user's entire Documents directory. `fs.delete` already gated directories correctly, so the three operations disagreed. Destinations now go through one guard shared by move and copy: a sandbox root is refused outright, an existing destination requires `overwrite: true`, and an existing *directory* additionally requires `recursive: true` — the same shape `fs.delete` enforces. `fs.delete` picks up the root check too, since `{path: 'documents:', recursive: true}` had the same reach. Also bounds and clarifies file IO: - `CodeModeConfiguration.fileSystemLimits` caps `fs.read`/`fs.write` (16 MB default). A 2 GB file in `documents:` was previously read whole and base64-serialized. The read cap is checked against stat before the read, so it does not spend the memory it exists to protect; the write cap is checked before the parent directory is created, so a refused write leaves nothing behind. - A `utf8` read of undecodable bytes now fails with a repair suggestion instead of silently returning `""`, which told the script the file was empty. - `encoding` is a `CodeModeStringEnum`, so the catalog advertises `allowedStringValues` instead of leaving the constraint in prose. `PathPolicy` gains `allowedRoots` (defaulted empty, so custom policies still compile) to make "is this a root" answerable. Golden metadata baseline regenerated; the diff is the four fs entries above. Review finding P0 #4, plus the `fs.read` encoding-constraint item from P2. * Strip ambient authority from network.fetch The shipping default was `URLSession.shared`, which reads `HTTPCookieStorage.shared` and `URLCredentialStorage.shared`. Every script request therefore rode whatever the host app was already logged in to — authenticated session-riding against any origin with a live cookie — and a `Set-Cookie` in a script-fetched response poisoned the app's cookie jar. The tests all passed an `.ephemeral` session, so the shipping default was the one configuration never exercised. `network.fetch` now defaults to `NetworkBridge.isolatedSession`: ephemeral, with no cookie storage, no credential storage, no shared cache, and cookies disabled outright. Per-request cookie handling is disabled too, so a host that supplies its own session still does not attach its jar. Caller-supplied `Cookie`, `Authorization`, and `Proxy-*` headers were copied to the wire verbatim; they are now refused with `NETWORK_POLICY_VIOLATION` and audited. `NetworkAccessPolicy.allowsCredentialHeaders` re-enables them for hosts whose scripts legitimately call an authenticated API, and `.permissive` sets it, keeping that opt-out a faithful "old behavior" switch. Review finding P0 #5. * Bound what a single execution can accumulate in memory Nothing capped output. The whole result was stringified, copied into a Swift String, and re-decoded; every console.log was retained and then copied again into each CodeModeToolError; the events AsyncStream used the default unbounded buffering, so a chatty script whose events the host was not draining buffered without limit. One script could jetsam an iOS host. `CodeModeConfiguration.executionLimits` bounds all of it, and every limit degrades rather than fails — for an LLM consumer a truncated result with a diagnostic beats an out-of-memory kill: - Result size is measured *inside* JS, so an oversized value is never brought across. Over the limit, the output is replaced by a still-parseable descriptor (`__codeModeTruncated`, character count, limit, preview) plus a RESULT_TRUNCATED diagnostic suggesting fs.write + return the path. - Retained logs are capped at the first N with a one-time LOG_LIMIT_REACHED notice; streaming stays live past the cap since it is the retained copy that costs. Over-long messages are elided in the middle, keeping head and tail. - Diagnostics and permission events are capped. - The events stream uses `.bufferingNewest(n)`. Two watchdog findings in the same code path: - The serialization re-arm used `max(timeoutMs, 1000)`, so a 60s execution bought another 60s for JSON.stringify. It is `min` against a fixed 1s budget now, which is what "bounded budget" meant. - `watchdog.termination` was never consulted after the re-arm, so a runaway getter surfaced as `INVALID_RESULT: must be JSON-serializable` and steered the model to the wrong fix. It now reports EXECUTION_TIMEOUT/CANCELLED naming the serialization phase. Review finding P0 #6 and the first two P2 items. * Fix two threading bugs in the permission and EventKit paths **Location.** `CLLocationManager` delivers delegate callbacks on the run loop of the thread that *created* it, and the bridge's GCD workers have none. The manager and delegate were constructed on the execution thread with only `requestWhenInUseAuthorization()` dispatched to main, so `locationManagerDidChangeAuthorization` never arrived: every first-time grant burned the full 10s wait and then reported from a status re-read that races the TCC write, so a user who tapped Allow could still get `notDetermined`. Creation, delegate assignment, and the request now all happen on main, the delegate holds the manager alive (CoreLocation does not retain its delegate, and the manager would otherwise die with the dispatched block), and the resolved status comes from the callback rather than a re-read. **Reminders.** `EventKitBridge.readReminders` wrote `var result` from EventKit's completion queue, read it after a `semaphore.wait` whose timeout result was discarded, and returned `.array([])` on timeout — a data race with the late callback, and a result the script could not tell apart from "no reminders". Both now go through one `CompletionWait` helper that either returns the value the callback delivered or throws `BridgeError.timeout`, keeping the box alive so a late callback writes somewhere harmless. The broker's `request*` methods are migrated to the same helper, which also collapses their duplicated availability-branch semaphore plumbing. Review findings P0 #7 and #8. * Work through the P2 correctness and polish findings **One ISO 8601 parser.** Health and Alarm fell back to `.withFractionalSeconds`; EventKit, Notifications, and the system-UI validators used a bare `ISO8601DateFormatter`. So `2026-07-24T10:00:00.000Z` — what JavaScript's `toISOString()` emits, and what models therefore produce by default — parsed in some bridges and not others. Worse, `calendar.read`/`reminders.read` then did `isoDate(...) ?? Date()`, handing the script a plausible but wrong window with no diagnostic. `CodeModeDate` is now the only parser, and a present-but-unparseable date is an error; only an absent one takes the default. **HealthKit stops claiming access it cannot verify.** `requestAuthorization`'s `success` means the sheet flow completed — HealthKit deliberately hides read denial so an app cannot infer a condition from it — but the bridge reported `granted: true`, and mapped `getRequestStatusForAuthorization == .unnecessary` ("nothing left to ask") to `.granted`. Reads then returned [] while the transcript claimed permission. It now reports flow completion, per-type *share* authorization (the only part HealthKit answers), and says plainly that read authorization is undisclosed. The broker's fail-closed `healthKitStatus` is untouched. **HomeKit status no longer prompts.** Constructing `HMHomeManager` is what triggers the TCC dialog, and `homeKitStatus()` built one inside `status(for:)` while `requestHomeKitPermission` called it twice and built a third. Status now answers from a shared manager if one exists and otherwise fails closed with `.notDetermined`, routing the caller to the request path where a prompt belongs. **Network and browser destinations.** Bare single-label hosts (`router`, `intranet`, `wpad`, `nas`) resolve onto the LAN through the DHCP search domain but fell through to "allowed"; they are refused like `*.local`, and an explicit `allowedHosts` entry still wins. `web.ui.present` and `auth.ui.webAuthenticate` now consult `NetworkAccessPolicy` — opening a page in a browser was exempt from the policy governing fetch — and `auth.ui.webAuthenticate` defaults to an ephemeral browser session, so a script-started OAuth flow no longer begins already signed in as the user. **Crash and containment fixes.** `EKEvent.calendar` (`EKCalendar!`, nil for orphaned events) and `EKReminder.title` (`String!`) are nil-coalesced like every neighbouring field. Two force-unwrapped `URL(string:)!` in the public `UIKitSystemUIPresenter` throw instead. A symlink to a *nonexistent* out-of-root target passed containment, because `fileExists` follows links (so the link read as a missing component) and `resolvingSymlinksInPath` leaves a dangling link unchanged; both halves are fixed, with a bounded walk for cycles. **Concurrency honesty.** `Task.isCancelled` was checked from plain GCD workers where it is always false — removed, leaving the controller that `cancel()` actually sets. The watchdog deadline is a `ContinuousClock.Instant`, so an NTP adjustment cannot move it. `JavaScriptExecutionCall` cancels on deinit, so a dropped call stops instead of running to completion in the background, and `result` no longer spawns a `Task.detached` per access. **Diagnostics.** Error-severity diagnostics were recorded but never emitted, so a host watching the stream saw every diagnostic except the ones that mattered most. **Path arguments.** `speech.file.transcribe` and `passkit.pass.add` left the raw script-supplied `path` in the dictionary alongside `resolvedPath`; the raw value is now replaced, so a host adapter reading `path` cannot bypass the path policy. Golden baseline regenerated for the two web-auth metadata strings. * Stop multi-step eval scenarios from masking a failed step `EvalRunner` overwrote `executionOutput`/`Logs`/`Diagnostics`/`observedError` on every step and did not stop on an intermediate error, so validation only ever saw the last one: a scenario could pass while step 1 failed in a way step 2 masked, and step 1's logs — the evidence for why — were discarded before anyone could read them. Logs and diagnostics now accumulate across steps. The graded output and error are still the last step's, so a successful repair step clears the failure that preceded it, but an unexpected failure stops the run. "Unexpected" needs to be explicit, because repair scenarios deliberately call a helper wrong and fix it from the structured error — that first failure is the whole point. `CodeModeEvalExecuteStep.expectsFailure` marks those, and `filesystemRepairAfterInvalidArguments` moves to explicit steps to use it. Every other step failing now ends the scenario. * Record the decision to keep swift-syntax in the core target REVIEW.md P1 #12 asks the CodeMode target to drop its CodeModeMacros dependency by checking in the 117 expanded tool definitions. Declined, with reasoning in TODO.md: the cited cost is already measured and mitigated (prebuilt swift-syntax is default-on, and the version range is already widened for the one named conflict), the migration is one-way and strips @ToolParam hints off the property declarations, and it pulls against findings #11 and #13, which both want more generation from this metadata. * Generate TypeScript declarations from the capability registry Cloudflare's central Code Mode insight is that models write markedly better code against real types, and nothing here emitted any: the model got a flat `argumentTypes` map of six coarse cases, a prose `resultSummary`, and constraints that lived only in hint text it had to notice and obey. The registry already holds names, required/optional arguments, types, enum constraints, and hints, so `TypeScriptDeclarations` emits `.d.ts` from exactly that. Constrained arguments become string-literal unions — the constraint *is* the type. Dotted argument paths (`options.timeoutMs`) rebuild into nested object types. Hints become doc comments; capability ID, result summary, aliases, and the example ride along. Two entry points: - `JavaScriptAPIReference.dts` — per capability, so code-driven search stays the filter and TypeScript becomes the payload. The tool description now points the model at it. - `CodeModeAgentTools.typeDeclarations()` — the whole platform-filtered surface, namespaced, for a host system prompt. `capabilities()` is exposed alongside it, which hosts also needed for consent UI and for building a `CapabilityGrant`. Results are typed `CodeModeValue`: built-in bridges return untyped JSON dictionaries, so a narrower result type would be a fiction. Per-capability result schemas can tighten this later without changing the shape. Generating declarations surfaced real catalog-vs-binding drift, which is fixed rather than described (partial #11 — the hand-written bootstrap stays for now): - `apple.keychain.{get,set,delete}` advertised object arguments while the wrapper took positional strings, and the catalog's own example showed the positional form. Code written from the metadata sent "[object Object]" as the key. The wrappers accept both spellings; examples move to the object form. - `fs.promises.rename`/`copyFile` could not pass the `overwrite`/`recursive` flags that `fs.move`/`fs.copy` now require, so those aliases failed on any existing destination. - `apple.location.getCurrentPosition`/`getPermissionStatus` discarded a caller's arguments outright; they now default the mode instead of forcing it. - The tool description claimed "apple.* helpers take one object argument" as a blanket rule; it now scopes positional to `fs.promises.*` and points at `dts`. The Node-compatibility globals stay in a hand-authored preamble, because their positional convention is not something the catalog can express — that gap is exactly what #11 exists to close. * Give the runtime a real timer loop and stop hiding swallowed failures Partial P1 #9. The full async bridge ABI — Swift-resolved promises, concurrent native calls, bounded execution slots — remains a milestone; these are the parts that are wrong *within* the synchronous model, including both cheap wins the review called out as available immediately. **setTimeout was a lie.** It invoked the callback synchronously and ignored the delay, so three things were wrong at once: `clearTimeout` could never cancel, an error thrown by a callback propagated to setTimeout's *caller* rather than being an uncaught task error, and `await new Promise(r => setTimeout(r, 2000))` — the standard backoff — became a hot loop hammering remote APIs through `fetch`. There is now a real timer queue drained by the host between JavaScript turns: callbacks are deferred, delays are honoured, registration order breaks ties, `clearTimeout` works, and a throwing callback becomes a `TIMER_CALLBACK_ERROR` diagnostic. Waiting stays responsive to `cancel()` and bounded by `timeoutMs`, so a 30s timer under a 100ms budget still times out at 100ms. **Unsettleable promises are diagnosed, not waited out.** `waitForSettlement` polled a state that, under this model, only a timer can change — microtasks have already drained inside `evaluateScript` and no native call is outstanding. With no timer queued the promise is *provably* unsettleable, so it reports `JS_RUNTIME_ERROR: ... can never settle` immediately instead of sleeping a thread to the deadline for a result that cannot arrive. **Swallowed bridge failures are visible.** Nothing installs an unhandled-rejection hook, so a forgotten `await` — the most common LLM JavaScript mistake — discarded a `CAPABILITY_DENIED` and the agent was told the run succeeded. A script that completes successfully despite failed bridge calls now carries a `BRIDGE_FAILURES_NOT_SURFACED` warning naming them. It is a warning, not an error, because a deliberate try/catch is indistinguishable from a missing await. Tool description and README updated: setTimeout is no longer described as synchronous, `Promise.all` is documented as sequential (which it is, until the async ABI lands), and the new diagnostics have repair guidance. The eval suite splits the old "unresolved promises time out" scenario into an unsettleable-promise case and a genuine CPU-bound timeout, and gains a timer-backoff scenario. * Bound concurrent executions and cover the concurrent path with tests The execution queue is `.concurrent` with no width limit, and each execution blocks its thread for the whole run, so thirty parallel `executeJavaScript` calls meant thirty blocked GCD threads and thirty live JavaScriptCore VMs. There was also no concurrent-execution test at all, despite the concurrent queue being a core design point. `ExecutionLimits.maxConcurrentExecutions` (default 8) gates the queue with a semaphore acquired on the worker rather than before dispatch, so the caller's async context is never blocked and waiting for a slot does not eat the per-execution `timeoutMs`. Two tests: twelve executions through three slots all complete with correct, isolated results and no state carried between contexts; and executions beyond the limit queue rather than fail. Closes the remaining "bound on concurrent executions" item in TODO.md §1. * Unfreeze the catalog and bring host providers toward built-in parity Partial P1 #13. The provider-per-method macro and the MCP adapter remain; this removes the blocker they both sit behind and closes the metadata gaps. **The catalog was frozen at init.** `BridgeCatalog` snapshotted the registry in `CodeModeAgentTools.init`, so `CapabilityRegistry`'s public post-init `register(...)` methods were unreachable in the supported flow — and if reached, would have desynced what search advertises from what execution can invoke. The registry now carries a generation counter and the catalog rebuilds when it changes, so the two can no longer disagree. `CodeModeAgentTools.register(provider:)` exposes this: a host whose domain APIs depend on runtime state — a signed-in account, a loaded document — can register them when that state arrives, and search sees them immediately. **Host providers can express constrained values.** `@CodeMode` rejected any non-primitive property type, so a host's `allowedStringValues` was always empty while built-ins advertised theirs — the model had no way to learn a provider's accepted spellings. An unrecognized simple type is now taken to be a `CodeModeStringEnum`, exactly as `@BuiltInCodeMode` already assumed, and its accepted values reach the catalog, the generated TypeScript union, and argument validation. A denylist of common non-enum types (`Date`, `URL`, `UUID`, …) keeps the "unsupported property type" diagnostic pointing at the property rather than letting a natural mistake fail inside generated code. **Generated metadata is no longer vacuous.** `example` was always `await path({})` regardless of arguments; it is now a call the model can pattern-match, with a placeholder per required argument. `tags` was only the parent path, so `myapp.tasks.complete` was findable only by searching "myapp.tasks"; every path segment is now a tag. * Cover the EventKit serialization crashes, pin CI, record what is left EventKit's serialization logic executed nowhere — not in unit tests, not in CI, not in evals — because the bridge hard-instantiates `EKEventStore` and the tests can only reach permission denial. The two implicitly-unwrapped fields that crashed the execution thread are now covered directly by constructing the model objects, which needs no permission: an `EKEvent` with no calendar and an `EKReminder` with no title. CI runners are pinned to `macos-15` rather than `macos-latest`, so the package's declared macOS 15 floor cannot drift without a code change. TODO.md gains a "review follow-up — still open" section listing every part of REVIEW.md's P1 items that was not completed — the async handler signature and concurrent Promise.all, native-call interruption, generating the JS bootstrap, `@CodeModeProvider`/`@CodeModeTool`, the MCP adapter, the remaining bridge store seams, a simulator job, and the "prompt pending" permission status — so the finished parts are not mistaken for the whole. It also records that the review's claim about missing `FetchTaskHandler` redirect and size-cap coverage was stale; both are covered in NetworkAccessPolicyTests. * Simplify the review-fix code: dedupe, drop dead code, tighten seams Quality pass over this branch's changes. No behavior change except one bug the refactor exposed, noted below. Reuse: - One `BridgeInvocationContext.toolError(...)` replaces three separate places assembling `CodeModeToolError` from the same three transcript accessors — the runtime's `toolError`, `SettlementParameters.error`, and `terminationError`. It belongs on the context, which owns those fields. - One `stringArray(from:evaluating:)` replaces the duplicated `JSON.stringify` → decode `[String]` round-trip in `rejectedSuggestions` and the timer-error read. - One `FileSystemBridge.overLimit(...)` replaces three hand-built size-limit messages that had already drifted apart in wording. Simplification: - `SettlementParameters` bundles the six values `waitForSettlement` and `runDueTimers` both needed; the two signatures drop from 7 and 8 parameters to 2 and 3. `checkInterrupted` replaces the cancellation/termination/deadline ladder both were repeating. - The host-withheld denial branch is hoisted above the `switch` instead of being nested identically inside two cases. - `decodeOutput` checks the watchdog once rather than side by side in a guard's else and again after it; its embedded JS returns the in-range value from one place instead of two. - `ExecutionTranscript.record(log:)` now matches the shape of `record(diagnostic:)` — one lock/unlock, no branch-local unlocking — and drops `droppedLogs`, which was incremented and never read. - `TypeScriptDeclarations.functionSignature` drops an unused `name:` parameter, and `lastComponent(of:)`, which existed only to supply it. - `CapabilityGrant.Resolution` keeps `withheld: [String]` instead of two sets that no caller read except through the accessor that joined them. - Dropped `CodeModeDate.string(from:)` — never called. Efficiency: - `advanceTimers` returns its callbacks' uncaught errors instead of buffering them for a second `takeTimerErrors` call, halving the bridge round-trips per timer tick. - The elapsed-time computation reads the clock once through a `Duration` extension rather than twice through a two-term attosecond conversion, and drops a `max` that could not fire. Bug found while refactoring: bundling the interrupt checks moved the deadline check ahead of the settled-state read, which is exactly the spurious-timeout regression the original comment warned about — a script that fulfilled a hair past the deadline would have had its result discarded. Ordering restored (cancellation before the read, deadline after) and pinned by a new test, which nothing covered before. * Teach agents the paradigm, not just the API surface The tool descriptions and the generated TypeScript answer "what is this helper called and what does it take". Nothing answered "what shape should my work take", so an agent could reasonably use executeJavaScript as a thin RPC — one call per operation — which is exactly the pattern this package exists to replace. Every code example in the package was a *search* example; executeJavaScript had none. `CodeModeAgentGuidance` supplies the missing half, to pair with `typeDeclarations()`: one call runs the whole task, filter and aggregate in the script, return the graded answer rather than the raw data, request only the capabilities you call. Three tiers with an optional budget selector, since prompt space is the scarce resource: - `.brief` (~160 tokens) — the core idea alone - `.standard` (~580) — adds the search → write → repair workflow and a worked multi-step example - `.full` (~1000) — adds partial-failure handling, the sequential-native-call reality so Promise.all is not over-promised, and the anti-pattern list `systemPrompt(approximateTokenBudget:)` picks the largest tier that fits. Tiers are additive — a smaller one loses examples, never a rule, which a test pins by substring containment. Token counts are estimated at four characters per token and documented as a planning bound, not a real tokenization. A budget too small for any tier still returns `.brief` rather than nothing. Also in executeJavaScript's description: the paradigm now leads instead of the namespace tour, three worked examples replace having none, and capability minimization is stated plainly — the eval suite asserts on it 38 times but the description mentioned it once in passing, so we were grading an unstated rule. Guarded, because documentation that names a helper the runtime lacks teaches the exact failure it is trying to prevent: tests check every helper and argument in both the guidance and the tool description against the live catalog, and run the worked example end to end. That caught a wrong example on the way in — `contacts.read` takes `identifiers` (a plural array), so the per-item loop it showed was the anti-pattern, not the pattern; it is now the example for "prefer one call that takes a collection". The new `fs.whole-job-one-script` eval scenario grades the standard rather than just asserting it: unit tests confirm a transcript splitting one job across three executions fails on tool order, an over-broad capability list fails even with the right answer, and the single-script transcript passes. * Fix CI: iOS/visionOS build breaks and load-sensitive test bounds Four failures on PR #7, all reproducible locally with the exact CI commands. **iOS build — a real break in my own CoreLocation fix.** `LocationPermissionDelegate` crosses an isolation boundary: the manager is created inside a `DispatchQueue.main.async` block, which is main-actor-isolated on iOS, while `wait`/`observedStatus` are called from the requesting thread. Swift 6 rejects sending a non-Sendable delegate across that. It is genuinely thread-safe — all mutable state is lock-guarded, the handoff is a semaphore — so it is now `@unchecked Sendable` with the reasoning recorded. `swift test` on macOS never caught this because `requestLocationPermission` is inside `#if canImport(CoreLocation) && os(iOS)`, so the macOS build never compiled the branch. Exactly the gap TODO.md flags about iOS being build-only. **visionOS build — latent, surfaced by pinning the runner.** `openSystemURL` calls main-actor-isolated `UIApplication.shared.open` from a nonisolated synchronous context. This is pre-existing code I did not touch; pinning `macos-latest` to `macos-15` moved the toolchain to one that diagnoses it. Fixed with `MainActor.assumeIsolated` on the branch that has already established it is on the main thread — a hop would return before the open was requested. **Three load-sensitive test bounds.** Individual tests took 8-9s on the CI runner against ~0.1s locally, so wall-clock upper bounds written on an idle machine failed. Each of these is a hang-guard, not a performance assertion, and the thresholds now say so: - `setTimeoutActuallyWaitsTheRequestedDelay` keeps its lower bound, which is the actual claim (the delay was previously ignored entirely); the upper bound is dropped since the script's own 5s timeoutMs already fails a runaway wait. - The remaining bounds are loosened to stay well under the thing they prove did not happen — 20s against a 30s budget, 15s against a 20s timer. - `completionWaitToleratesALateCallback` waits 60s. The starvation is real: parallel tests plus this runtime's thread-blocking timer waits can leave a 0.2s dispatch waiting seconds for a GCD worker. That is a property of the synchronous bridge model, and one more argument for the async ABI in #9. Also fixes the `variable 'root' was never mutated` warning CI surfaced in TypeScriptDeclarations. Verified locally with the exact CI invocations: `swift test` (322 passing) plus `xcodebuild -destination generic/platform={iOS,visionOS}`, both succeeding. * Fire timers in due order, and stop two tests starving on GCD CI caught a real bug in the timer queue, not just a flake. `advanceTimers` sorted due timers by registration id alone. That is correct only when everything came due at the same instant. When the host's sleep overshoots far enough that two timers with *different* delays land in one tick — which is routine on a loaded machine — the later one fired first purely because it was registered first. Timers now sort by due time, using registration order only to break ties, which is what a real event loop does. The existing test could not catch this: with delays of 5ms and 10ms it only fails when the sleep overshoots, so it passed locally and failed on CI. It is joined by one that reproduces the condition deterministically — delays below the host's sleep granularity, so both timers reliably come due in a single call — plus one covering the same-instant tie-break the id sort did get right. Verified by reverting the fix: the new test fails, the old one still passes. Separately, `completionWaitReturnsTheDeliveredValue` timed out waiting 5s for an immediate `DispatchQueue.global().async`. That is thread-pool starvation: the suite runs in parallel and this runtime's timer waits block GCD workers with `Thread.sleep`, so a trivial dispatch can wait seconds for a thread. The timeout is now 60s, since the assertion is that the value arrives rather than how fast. Worth recording that the starvation is a property of the synchronous bridge model — the async ABI in #9 is what actually removes it. * Fresh-look pass: fix two lifetime/concurrency bugs and tighten the rest A second read of the branch as a reviewer would, rather than as its author. Two of the findings are correctness bugs in code this branch added. **Cancel-on-deinit cut off hosts that only iterate events.** The P2 fix made `JavaScriptExecutionCall` cancel in its `deinit` so a dropped call stops running. But Swift does not keep a local alive to the end of its scope: a host that reads `call.events` and never touches `call` again can have it released mid-iteration in an optimized build, and the deinit then cancelled the execution out from under the `for await`. The README Quick Start avoids this only because it awaits `call.result` afterwards. The right semantic is "cancel when nothing can observe the outcome" — so an `ExecutionObservationToken` is now held by both the call and the stream's continuation storage, and the execution runs while *either* is reachable. Two tests discriminate by construction: iterating events after explicitly releasing the call must see `finished`, and dropping both must free a single-slot runtime for the next call. **The concurrency bound blocked the threads it was protecting.** The slot semaphore was acquired inside `executionQueue.async`, so every execution beyond the limit held a GCD worker while waiting — exactly the resource the limit exists to conserve. `ExecutionSlots` is an actor; waiters park as suspended tasks, a freed slot hands directly to the next waiter, and a call cancelled while parked throws before anything is dispatched. Tested: a call cancelled while waiting reports CANCELLED and never ran (the file it would have written is absent). **The timer loop polled.** `runDueTimers` slept in 5ms slices — 200 wakeups a second per sleeping execution, and up to 5ms of cancellation latency — which is what made CI's thread starvation visible. `ExecutionCancellationController` now owns an `NSCondition`: one interruptible wait until the timer is due or the deadline passes, woken immediately by `cancel()`. The wall-clock `Date` is only the wake-up hint; the loop still re-checks against `ContinuousClock`, so an NTP adjustment cannot move either the timer or the deadline. **Smaller findings:** - The per-capability `dts` flattened `apple.fs.read` to `declare function apple_fs_read`, a name that does not exist. It is now declared at its real call path; bare globals like `fetch` are declared directly. - `health.permission.request` changed its result shape in the P2 commit but its advertised `resultSummary` still promised a `granted` field. Golden regenerated. - `HostConfigurationValidator.validate(configuration:)` warns when `capabilityGrant` is `.unrestricted`, and the README Quick Start — the thing that gets copy-pasted — now models a grant instead of the unenforced default. - `hostWithheldCapabilityIdentifiers` was public API for internal bookkeeping. - `ExecutionLimits` is documented as bounding concurrency too, not just "a single execution", since `maxConcurrentExecutions` lives there. - `CodeModeAgentGuidance.Length` drops `Comparable` and a private ordering it carried only so the budget selector could sort; `allCases` already is the order. Verified with the exact CI invocations: `swift test` (331 passing) and `xcodebuild -destination generic/platform={iOS,visionOS}`. * Synchronize the sleep-interrupt test on the sleeper actually starting The test cancelled 50ms after *dispatching* the sleeper block. On a starved CI runner the dispatch alone can take seconds, so the cancel arrived before there was anything to wake, the sleeper only ran after the test had given up, and the failure read as "cancel did not wake the sleep" when the sleep had never begun. It now waits (generously) for the block to signal that it is about to sleep, then cancels and measures only the wake-up — which is the property under test. If the cancel lands in the window before the wait starts, `sleep(until:)` sees the flag on its first check and returns immediately, so there is no hang and no false positive. * Stop the test suite from starving itself on CI The last CI run failed four tests at ~66 seconds each — including two that need nothing but a single GCD thread. That is a runner-wide stall, and the suite was partly causing it: seven watchdog tests each spin a core at 100% for their budget, and a dozen event-loop tests each hold a GCD worker for the length of a timer, all dispatched in parallel onto a three-core runner. Serializing those three suites keeps them from stacking on each other; cross-suite parallelism is unchanged. Two tests were also asserting more than their property, which is why a stall read as a runtime bug: - `executeCancelsWhenAwaitingTaskIsCancelled` cancelled after a `Task.yield()`. Under a stall the test task can be descheduled past the script's 20s timer, and cancelling a finished execution is a no-op — reported here as the script "ignoring" cancellation. It now cancels only after observing the script's own `running` log on the events stream, which makes the outcome CANCELLED regardless of scheduling. - `anUnsettleablePromiseIsReportedImmediately` and `aTimerStillPendingAtTheDeadlineTimesOut` carried wall-clock bounds that their error codes already proved: JS_RUNTIME_ERROR rather than EXECUTION_TIMEOUT shows the 30s budget was not run out, and EXECUTION_TIMEOUT rather than a returned `1` shows the 30s timer was not waited out. Dropped. No product code changed. Run locally: 331 passing. * Declare CodeMode as a direct dependency of the authoring tests `CodeModeAuthoringTests` uses `CodeModeConfiguration` and `CodeModeAgentTools` but declared only `CodeModeAuthoring` and `CodeModeMacros`, reaching `CodeMode`'s symbols through re-export. SwiftPM did not treat that as an edge for rebuild invalidation, so whenever a `CodeMode` type changed layout the authoring test objects went stale — and a plain `swift test` then either failed to link ("Undefined symbols: CodeModeConfiguration.init(...)") or crashed with signal 11 partway through the run. Every such failure this branch hit was worked around by touching the test sources; this is the fix. Verified by reproducing the trigger: add a stored property to `CodeModeConfiguration`, run `swift test` with no workaround. Before: link failure or segfault. After: the authoring tests rebuild and all 331 pass. * Make the timer tests deterministic with an injectable clock The timer tests were the least reliable tests in the suite, and the earlier fixes — serializing suites, widening bounds — reduced the failure rate without removing the cause: they slept real time and asserted wall-clock bounds, so a loaded CI runner turned a 150ms wait into seconds and every assertion became a guess about how slow the machine was allowed to be. `RuntimeClock` is now the runtime's source of time for the two places that wait: the timer loop and the watchdog deadline. `RealClock` is production and unchanged. `VirtualClock` makes `sleep(until:)` a jump, so under test a timer fires instantly and exactly, and a test can assert on how long the runtime *asked* to wait rather than on how long the machine took: - `setTimeoutActuallyWaitsTheRequestedDelay` asserts `clock.elapsed == .milliseconds(150)` — exact, where it previously asserted `>= 0.14` real seconds with no upper bound it could safely hold. - `aTimerStillPendingAtTheDeadlineTimesOut` asserts it stopped at exactly 100ms, never anywhere near the 30s timer. - `anUnsettleablePromiseIsReportedImmediately` asserts `elapsed == .zero` — the runtime never slept at all. - `aResultThatSettlesRightAtTheDeadlineIsNotDiscarded` was a 12-iteration race hoping to land a timer on the deadline; it is now one exact case with timer and deadline at the same instant. `VirtualClock(overshoot:)` models a slow machine precisely — every sleep wakes late by a fixed amount. That matters for `timersComingDueInOneTickStillFireInDueOrder`: after the earlier switch from 5ms polling to an interruptible wait, real sleeps stopped overshooting enough to bring two timers due in one tick, so that test had quietly stopped exercising the condition it exists for. With a 10ms overshoot it reproduces it on every run. Both converted regression tests were mutation-checked: the overshoot test fails against the id-only sort, and the exact-deadline test fails against a deadline-before-settled-state check — the two bugs they guard. Kept on the real clock deliberately: watchdog termination of a CPU-bound script (driven by JavaScriptCore's own execution-time callback, which virtual time cannot advance) and the cancel-mid-sleep tests, which are already outcome-deterministic because they synchronize on observable events rather than durations. The seam is internal — `CodeModeAgentTools.init(config:clock:)` is not public. Production time is not a host decision. Verified with the exact CI invocations: `swift test` (331 passing) and `xcodebuild -destination generic/platform={iOS,visionOS}`. * Close the source side of fs.move/fs.copy, and make overwrite non-destructive The P0 #4 fix guarded the destination against sandbox roots but never checked the source. `apple.fs.move({ from: 'documents:', to: 'tmp:stolen' })` succeeded and moved the user's whole Documents directory into tmp:, which the OS may purge; `fs.copy` from a root duplicated the entire tree with no size bound. Both sides are now refused, matching fs.delete. The overwrite path also removed the existing destination *before* the move or copy ran, so any failure after that point lost it: - a missing source with overwrite: true deleted the destination, then threw; - `fs.move({ from: 'tmp:x', to: 'tmp:x', overwrite: true })` deleted x itself; - a full disk or permissions error mid-copy did the same. The destination is now moved aside (a same-directory rename), the operation runs, and only on success is the old item removed; on failure it is moved back. Validation also runs first: the source must exist, and the destination may not be the source or a path inside it. `fs.copy` now answers to `maxWriteBytes` — it writes new bytes, and was otherwise a way around the write limit. Directory sizes are walked with an early exit, so a huge tree is refused without being traversed in full. Tests: both root-source cases, missing-source overwrite, self-overwrite, destination-inside-source, a copy over the limit made of individually small files, and a filesystem whose copy fails after the aside move — the destination must come back intact with no aside file left behind. * Render TypeScript declarations as object types so reserved words are legal The generated declarations used `declare namespace`, whose members are declarations and need identifiers. Helpers named with reserved words — `apple.keychain.delete`, `apple.photos.export`, `apple.documents.export` — rendered as `function delete(…)` and `function export(…)`, which is not valid TypeScript. Any host that type-checks the output failed on it. Roots now render as `declare const apple: { keychain: { delete(args: …): …; }; };`. Object type members may be any property name, and the shape reads the same to a model. Each per-capability fragment is valid on its own; `surface(for:)` remains the way to get one compilation unit covering several capabilities, since it merges them under a single declaration per root. The new test checks the whole iOS surface against the full ECMAScript reserved word list. Verified against a real compiler would be better still, but the local `tsc` shim points at an uninstalled TypeScript, so that check is left for CI or a machine with a working install. * Stop the agent docs from spending tokens hosts can't budget Two places the documentation work undercut its own token budgeting. The `executeJavaScript` description had grown from 2,711 to 5,802 characters, most of it three worked examples. Tool descriptions go out on every turn and a host cannot trim them — which is exactly why `CodeModeAgentGuidance` has budgeted tiers. The examples now live only there; the description is 4,656 characters, the remainder over main being the paradigm sentence, the concurrency model, and the diagnostics guide. The one rule the examples carried that the guidance lacked — check for an argument that takes a collection before writing a loop of single calls — moves into the guidance workflow. `typeDeclarations()` returned the whole platform surface, roughly 21,000 tokens on iOS, while the docs paired it with a ~580-token guidance tier. It now follows the host's `CapabilityGrant`: a host granting three helpers gets about 1,000 tokens, most of it the shared preamble, and never advertises a helper the model would be denied. `typeDeclarations(for:)` takes an explicit subset for when even the grant is too broad. `CapabilityGrant.permits(_:)` / `permits(key:)` back both. Also corrects the docs' claim that over-broad capability lists are "refused": with a ceiling, the extra capability is withheld and the run continues with a warning. The guidance now says what is true — it widens the grant for nothing. Tests: a parser over every `apple.*({ … })` call in the description and all guidance tiers checks each helper and argument against the live catalog, so new examples are covered without updating a list; a size guard stops the description regrowing examples; and declarations are checked to narrow with a grant, narrow to an explicit subset, and cover everything when unrestricted. * Decode JavaScriptAPIReference without a dts key Adding `dts` as a non-optional stored property made the synthesized decoder require it, so a reference encoded before this branch — a host's cached search results, an eval fixture — failed to decode with `keyNotFound: dts`. `dts` is now optional on decode, and when it is absent the declaration is regenerated from the decoded fields, so a legacy reference comes back with the same declaration a fresh one would carry rather than an empty string. Encoding is unchanged. Tests: the exact legacy shape decodes and gains a generated declaration, and the full live catalog round-trips through Codable unchanged. * Require PathPolicy.allowedRoots instead of defaulting it to empty The root guards on fs.move, fs.copy, and fs.delete can only recognize a root the policy names, and `allowedRoots` defaulted to `[]`. A host with its own `PathPolicy` therefore got none of the root protection — silently, with everything compiling and the guards never firing. That is a fail-open default on a safety check. The requirement now has no default, so a custom policy has to state its roots; one that genuinely has none to protect can return `[]` explicitly. This is a source break for custom conformers, intentionally: pre-1.0, and the alternative is a protection that quietly does nothing. `isAllowedRoot` keeps its default. Test: a host-style policy narrowing the default to two roots gets both the source- and destination-side root refusals, and the file it would have moved survives. README notes the requirement for hosts. * Give search its own slots, and drop a dead branch from ExecutionSlots Search shared the execution slots, so a host running long scripts at its concurrency limit stalled API discovery behind them — exactly when an agent is working out its next step. Search is cheap, capability-free, and bounded to 2s; it now has its own slot pool of the same size. `ExecutionSlots.withdraw` recorded ids it could not find "for later", on the theory that cancellation might beat the waiter into the queue. It cannot: `acquire()` is actor-isolated and parks the waiter synchronously, before any `withdraw` task from the cancellation handler can run on the actor. The only way to reach that branch was after `release()` had already handed the waiter a slot — and then the id was inserted and never removed. The branch is gone; a late cancel is a no-op, and the execution sees it through its cancellation controller. Test: with one execution slot held by a script that writes a file after 3s, a search returns before the file exists. The occupant is synchronized on its own `running` log rather than a sleep. * Bring TODO.md and the README's API list up to date TODO.md still described the concurrency bound as "a semaphore acquired *on* the worker" — the design this branch replaced because it blocked the threads it was protecting — and listed `runOnExecutionQueue` cancellation as "NOT addressed", which it now is at every stage. Both entries describe the current mechanism and name the tests that cover them. The README's Public API list predated every public type this branch added: `CapabilityGrant`, `ExecutionLimits`, `FileSystemLimits`, `CodeModeAgentGuidance`, `TypeScriptDeclarations`, and the new `CodeModeAgentTools` methods. It now lists them, grouped by where a host meets them. * Verify the watchdog on iOS, and make the suite honest about the keychain TODO.md has carried one pre-tag item since the start: JSC ships per-OS, and the watchdog had only ever been verified on macOS. The full CodeModeTests target now runs on an iPhone 16 Pro simulator (iOS 18.6) — the first time this suite has executed on iOS at all — and all seven watchdog tests pass there: CPU loop, loop in a promise chain, catch-proof termination, cancel mid-loop, runaway getter, serialization budget, and search. 315 tests pass. That run also failed six tests, all keychain, all with -34018 (`errSecMissingEntitlement`): an unsigned SwiftPM test bundle on the simulator has no keychain access group. An environment limit, not a bug — but it exposed two real problems: - The bridge reported "keychain.write failed with status -34018", which tells neither the model nor the host developer anything. Failures now carry Security's own description of the status, and -34018 specifically says the host app lacks a keychain entitlement and that retrying will not help. Host developers hit exactly this in their own unsigned test targets. - The tests could not tell "no keychain here" from "keychain broken". Tests whose subject is keychain storage now skip, with that reason, when a probe shows the entitlement is missing. The capability-gating test only touched the keychain incidentally, so it now asserts its actual subject — the call was not denied — and runs everywhere. On macOS every keychain test still runs and passes; on the iOS simulator exactly the four storage tests skip and the rest pass. Simulator, not device. * Correct what the declaration docs claim, now that tsc has checked it With a working TypeScript compiler (7.0.2) the generated declarations were checked for real rather than by proxy: - the whole surface for iOS, macOS, and visionOS each compile with zero errors under `tsc --strict`; - all 121 per-capability fragments compile when paired with the preamble. So the object-type rendering holds up, including the reserved-word helpers. But the doc comment on `declaration(for:)` claimed each fragment was "valid TypeScript on its own", and the compiler disagrees on two counts: a fragment references `CodeModeValue`, which only the preamble defines, and two fragments for the same root each declare it, so joined fragments do not compile as one unit. Neither is a defect — a model reading search results is unaffected, and `surface(for:)` / `typeDeclarations(for:)` already exist for compiling several capabilities together — but the comment now says exactly that instead of the stronger claim. Doc-only change. * Compile the generated TypeScript declarations in CI The declarations go into model prompts and into hosts' type-checkers, so whether they are valid TypeScript should be checked by a compiler, not inferred. The Swift tests only catch failure shapes someone already thought of — the reserved-word test exists because `function delete(` shipped once. `scripts/check-typescript-declarations.sh` writes every declaration the generator produces — the preamble, the unrestricted surface for iOS, macOS, and visionOS, and all 121 per-capability fragments — then compiles them with `tsc --strict`, TypeScript pinned to 7.0.2. It runs the same way locally (`TSC=tsc` to use an installed compiler) and in CI. Everything compiles in one pass rather than 124. The preamble stays a global script, as it is in a host prompt; each surface and fragment becomes its own module, so their top-level `declare const apple` stay local instead of colliding, and each is checked exactly as it would be used alone. The script refuses to pass vacuously: it fails if the dump produced fewer than 3 surfaces or 100 fragments, or if tsc would check fewer files than were written — an empty compilation succeeds, so a filter typo would otherwise go green. The dump is a Swift test that is inert unless `CODEMODE_DTS_OUTPUT` is set, so `swift test` is unchanged. In CI it runs right after the test step, when the build is already warm. Verified both ways: all 125 files compile clean, and reintroducing the original `function delete(` bug makes the script exit non-zero with 422 compiler errors. * Bound timer delays before converting them, so a script can't crash the host Found by Codex review. The timer loop converted the delay it read from JavaScript with `Int(delayMs.rounded(.up))`, and `Int(_: Double)` traps on anything non-finite or out of range. A capability-free `await new Promise(r => setTimeout(r, 1e300))` — or `Infinity` — killed the embedding app with "Double value cannot be converted to Int". Search runs the same loop, so it was reachable with no capabilities at all. This is the exact bug class P0 #3 removed everywhere else, reintroduced in the timer loop. The real fix is on the Swift side: the wait is bounded by the time remaining before the deadline *before* any conversion, so whatever arrives — 1e300, Infinity, NaN — the converted value is at most the remaining budget, which is always small. A huge timer now ends in an ordinary timeout, exactly at the deadline. The JavaScript side also normalizes delays, but that is hygiene, not a boundary: `__codemode` lives on globalThis, so a script can replace `nextTimerDelay` or push a timer entry directly and skip it. NaN, negative, and missing delays fire as soon as possible, as on the web; anything above 2^31-1 ms is clamped to it (browsers wrap such delays to fire immediately, which would turn `setTimeout(r, Infinity)` into a hot loop). Tests, under the virtual clock so each is exact and instant: unrepresentable delays time out at precisely the 100ms deadline; degenerate delays fire with zero elapsed; search survives 1e300; and three tampering scripts that bypass the JavaScript normalization entirely — replacing `nextTimerDelay`, or pushing an Infinity entry — still end in a timeout at the deadline rather than a crash. * Count hidden entries against the fs.copy limit Found by Codex review. The copy-limit walk listed directories through `LocalCodeModeFileSystem.listDirectory`, which passed `.skipsHiddenFiles`, while `FileManager.copyItem` copies everything. A 1,024-byte `.hidden` file copied successfully under a 4-byte limit, and a hidden file or subtree of any size could pass the default 16 MiB ceiling the same way. The root cause was a display choice living in the storage layer: hiding dotfiles is what `fs.list` wants to show a model, not what a filesystem abstraction should report. `listDirectory` now returns every entry, marking hidden ones with a new `CodeModeFileSystemEntry.isHidden`, and `fs.list` filters on that flag itself — so quota accounting sees everything while listings are unchanged. `isHidden` comes from the filesystem's own `isHidden` resource value, the same test `.skipsHiddenFiles` applied (dot-prefixed names plus the hidden flag), so `fs.list` hides exactly what it hid before. `isHidden` defaults to false, so existing code constructing entries still compiles. The `listDirectory` requirement now documents that it must include hidden entries; a custom `CodeModeFileSystem` that omits them would under-count the same way. That contract cannot be enforced at compile time, so it is stated where a conformer will read it. Tests: the reviewer's exact reproduction is refused; a hidden *directory* holding a large file is counted; fs.list still shows only visible entries while the storage layer reports the hidden one, marked. * Validate exception line numbers, so a script can't crash the host with one User code is spliced into an `(async function(){ … })()` wrapper, so a script can close it early and throw an arbitrary object at top level. The host read that object's `line` through `NSNumber.intValue`, which saturates -Infinity to Int.min, and the line adjustment then overflowed: `throw { line: -Infinity }` trapped, with no capabilities granted. Only a positive whole number in a plausible range is now accepted as a line or column, and the adjustment itself can no longer overflow. Pre-existing on main; found sweeping the other JS-to-Swift numeric reads after the timer-delay crash. * Mark the pre-merge verification done in TODO.md The iOS simulator run closed the last open item, but the heading was still half-checked and a parenthetical still said iOS was compile-verified only. visionOS genuinely is, and now says so on its own. * Compile scripts on their own, so they can't break out of the harness User code was spliced as text into `(async function(){ … })()`, so `return 1; })(); …; (async function(){` closed the wrapper and ran at top level, outside the harness's `.catch`. That is how a thrown `{ line: -Infinity }` reached the host and crashed it; the previous commit made the line read safe, this removes the way in. Search had the same shape: its expression was pasted into `const __search = (…)`. Both now hand the source over in a global and compile it with the `Function`/`AsyncFunction` constructors, which parse a body on its own, so it cannot close the function it is compiled into — a body that tries is a syntax error. A search expression can still close its own parentheses, but only into a function body it could write anyway as an async arrow. Syntax errors keep accurate lines (JavaScriptCore numbers the generated `function anonymous(\n) {\n` source, so the offset is 2 for both paths). They no longer carry a column: JSC reports the column of the constructor call, not the error, and the old column was off by the wrapper's indentation anyway. The agent guidance stopped promising one. * Rewrite the README as an overview, at about a third of the length It had grown into an accumulated reference: no explanation of how a call flows, the capability grant explained three times, a dump of the public API, and eval CLI flags that EVALS.md already covers. It now runs: what Code Mode is and why, install, quick start, how it works, wiring it into an agent, the security model, custom helpers, host setup, development. Dropped: the public API list, the search interface listing and event cases (the Swift types document both), the eval tolerance flags (in EVALS.md), and the stale "Shipped Expanded Families" / "Deferred in v1" sections. Added: a how-it-works walkthrough, the `@CodeMode` macro, which the README never mentioned, and the Info.plist keys as a table. Corrected against the code: `approximateTokenBudget: 400` selects `.brief`, not `.standard`; `.standard` is ~620 tokens now; there are 52 eval scenarios, not 49; and syntax errors no longer carry a column.
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.
Addresses the critical issues from a repo evaluation: timeouts that couldn't interrupt running JavaScript, an unrestricted
fetch(SSRF), and install docs that couldn't resolve. A high-effort self-review of the fixes then surfaced a compile blocker and several bugs, which are also fixed here.Runtime: real timeouts & cancellation
evaluateScriptcall, so the old wall-clock poll loop never ran while JS executed —while(true){}pinned a worker thread forever andtimeoutMswas never enforced. AddedExecutionWatchdog, which installs a JavaScriptCore execution time limit so CPU-bound scripts are preemptively terminated andcancel()interrupts in-flight JS.JSContextGroupSetExecutionTimeLimit/...Clear...are exported by JavaScriptCore but declared only in a private WebKit header, so they aren't visible through the public SDK module. They're reached through a new thin C shim target,CCodeModeJSC. This is JavaScriptCore's only interruption mechanism; the private-symbol dependency is documented in the README and the watchdog source so a host can make an informed App Store decision.waitForSettlementhelper), and result serialization ran with the watchdog uninstalled so a runaway getter/toJSONhung the thread forever (the watchdog now stays armed with a fresh bounded budget for the decode phase).Security: network egress policy (SSRF)
NetworkAccessPolicyonCodeModeConfiguration. The default blocks loopback, RFC 1918, link-local (including cloud metadata like169.254.169.254), CGNAT, and unique-local destinations — including alternate/encoded IPv4 literals and IPv4-in-IPv6 (IPv4-mapped, IPv4-compatible, and NAT64) forms — pluslocalhost/.local/.internalnames and fully-qualified trailing-dot spellings.allowedHosts/blockedHostsand.permissivegive hosts control. Allowed and denied egress destinations are written to the audit logger.Docs
from: "0.1.0"against a tagless repo (SPM can't resolve it); it documents branch-based install until0.1.0is tagged, and adds sections on the network access policy and the timeout/cancellation semantics.CI
platform-buildmatrix (xcodebuildfor iOS and visionOS) so the UIKit presenters and theCCodeModeJSCshim compile against those SDKs —swift teston macOS never did — plus SwiftPM build caching and toolchain logging.Tests
ExecutionWatchdogTests(infinite loops, loop-in-promise-chain, uncatchable timeout, settles-at-deadline returns result, infinite getter during serialization, explicit cancel, search timeout) andNetworkAccessPolicyTests(private/loopback/link-local/CGNAT, encoded IPv4, IPv6 incl. NAT64, trailing-dot, allow/deny lists, size cap, redirect refusal).Verification note
Authored in a Linux environment without a Swift toolchain or JavaScriptCore, so nothing here was compiled or run locally — this PR is opened specifically to let CI build and test it on macOS/iOS/visionOS. A remaining
TODO.md(committed on this branch) tracks the follow-up work the evaluation surfaced (JS heap cap, concurrency bound, HealthKit-always-denied, calendar-span validation drift, Security-layer tests, and more).🤖 Generated with Claude Code
https://claude.ai/code/session_01Az9HuLR4Sk6tP5Y924z1zF
Generated by Claude Code