From 473c80d13f2b63fb142b58a6273c0679cb65bd7a Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:11:43 -0500 Subject: [PATCH] Automatically import and update project repository skills (#16066) Automatically import skills from new GitHub project repositories and update enabled sources in the background. Preserve project-derived access, saved selections, immutable execution versions and explicit pins; add legacy adoption, refresh and connection repair in Sources. Include durable scheduling and bounded work credits, production Storybook journeys, real GitHub-to-routine acceptance receipts and final-head CI/security/Greptile qualification. Co-Authored-By: Paperclip --- doc/DATABASE.md | 44 + doc/DEVELOPING.md | 13 + doc/PRODUCT.md | 37 +- doc/SPEC-implementation.md | 84 +- .../2026-10-12-project-repository-skills.md | 201 ++++ .../2026-10-12-project-skills-acceptance.md | 517 ++++++++++ maps/agent-access-map/projects.md | 9 + maps/agent-access-map/routines.md | 6 + maps/agent-access-map/skills.md | 34 + maps/feature-map/connection-setup.md | 2 + maps/feature-map/projects.md | 8 + maps/feature-map/routines.md | 8 + maps/feature-map/skills.md | 15 + .../codex-local/src/server/execute.ts | 70 +- .../db/src/migrations/0341_abnormal_bucky.sql | 78 ++ packages/db/src/migrations/0342_dry_shen.sql | 7 + ...{0337_snapshot.json => 0341_snapshot.json} | 578 +++++++++++- ...{0336_snapshot.json => 0342_snapshot.json} | 719 +++++++++++++- packages/db/src/migrations/meta/_journal.json | 14 + .../project-skill-sources-migration.test.ts | 54 ++ .../db/src/schema/company_skill_sources.ts | 60 +- packages/db/src/schema/company_skills.ts | 2 + packages/shared/src/types/company-skill.ts | 2 + packages/shared/src/types/skill-source.ts | 24 + .../shared/src/validators/skill-source.ts | 9 + .../chat-channels.integration.test.ts | 11 +- .../src/__tests__/codex-local-execute.test.ts | 124 ++- .../__tests__/company-skills-routes.test.ts | 43 + .../src/__tests__/github-skill-source.test.ts | 27 +- .../heartbeat-execution-configuration.test.ts | 45 +- .../heartbeat-runtime-skills.test.ts | 40 +- server/src/__tests__/helpers/github-skills.ts | 6 +- .../src/__tests__/project-routes-env.test.ts | 10 + .../project-skill-setup-identity.test.ts | 25 + .../__tests__/project-skill-sources.test.ts | 893 ++++++++++++++++++ .../server-startup-feedback-export.test.ts | 6 + .../src/__tests__/skill-source-retry.test.ts | 19 + server/src/__tests__/skill-sources.test.ts | 149 ++- server/src/app.ts | 2 + server/src/index.ts | 14 + server/src/realtime/live-events-ws.ts | 8 + server/src/routes/company-skills.ts | 54 +- server/src/routes/openapi.ts | 11 +- server/src/routes/projects.ts | 17 + server/src/services/activity.ts | 5 +- server/src/services/company-portability.ts | 3 + server/src/services/company-skills.ts | 111 ++- server/src/services/github-skill-source.ts | 29 +- .../heartbeat/execution-configuration.ts | 62 +- .../src/services/heartbeat/run-preparation.ts | 11 + .../heartbeat/runtime-selection.test.ts | 22 + .../services/heartbeat/runtime-selection.ts | 10 + .../services/local-ai-browser-login.test.ts | 2 + .../paperclip-runner-tool-authority.ts | 17 +- .../services/project-skill-source-bindings.ts | 64 ++ server/src/services/projects.ts | 8 +- server/src/services/skill-access.ts | 99 ++ .../src/services/skill-source-git-snapshot.ts | 19 +- .../services/skill-source-github-access.ts | 16 +- .../services/skill-source-import-budget.ts | 32 + .../src/services/skill-source-import-queue.ts | 44 + .../services/skill-source-refresh-budget.ts | 43 + server/src/services/skill-source-retry.ts | 9 + server/src/services/skill-source-sync.ts | 314 ++++++ server/src/services/skill-sources.ts | 173 +++- tests/e2e/signoff-policy.spec.ts | 10 +- tests/storybook-visual/playwright.config.ts | 4 +- .../project-repository-skills.spec.ts | 202 ++++ ui/src/api/skillSources.ts | 5 +- .../ProjectSkillRepositoryRow.tsx | 27 + .../SkillSourceConnectionRepair.tsx | 50 + .../skill-studio/SkillSourceSyncControls.tsx | 23 + ui/src/context/LiveUpdatesProvider.test.ts | 11 + ui/src/context/LiveUpdatesProvider.tsx | 7 + .../ConnectionIntentInteractionBody.test.tsx | 9 + .../ConnectionIntentInteractionBody.tsx | 6 + ui/src/lib/queryKeys.ts | 1 + ui/src/pages/SkillSources.test.tsx | 5 +- ui/src/pages/SkillSources.tsx | 70 +- .../apps/chat/ChatExecutionDefaults.test.tsx | 18 +- .../pages/apps/chat/ChatExecutionDefaults.tsx | 18 +- ui/storybook/fixtures/githubSkillSources.ts | 9 +- .../automatic-attachment.png | Bin 0 -> 69491 bytes .../connection-repair-mobile.jpg | Bin 0 -> 34511 bytes .../execution-master-final-marker.txt | 1 + .../execution-review-cli-marker.txt | 1 + .../execution-v1-marker.txt | 1 + .../execution-v2-marker.txt | 1 + .../public/project-skills-evidence/index.html | 1 + .../repair-conflict-mobile.jpg | Bin 0 -> 34491 bytes .../routine-execution-cli.jpg | Bin 0 -> 93748 bytes .../routine-execution-final.jpg | Bin 0 -> 89112 bytes .../routine-execution-reviewed.jpg | Bin 0 -> 93326 bytes .../routine-execution.jpg | Bin 0 -> 80344 bytes .../sources-desktop.jpg | Bin 0 -> 64179 bytes .../project-skills-evidence/versions.json | 172 ++++ .../in-feed-connections.stories.tsx | 8 + .../project-repository-skills/README.md | 21 + .../components.stories.tsx | 23 + .../journeys.stories.tsx | 64 ++ .../support/projectRepositorySkills.ts | 217 +++++ 101 files changed, 5991 insertions(+), 186 deletions(-) create mode 100644 doc/plans/2026-10-12-project-repository-skills.md create mode 100644 doc/plans/2026-10-12-project-skills-acceptance.md create mode 100644 packages/db/src/migrations/0341_abnormal_bucky.sql create mode 100644 packages/db/src/migrations/0342_dry_shen.sql rename packages/db/src/migrations/meta/{0337_snapshot.json => 0341_snapshot.json} (98%) rename packages/db/src/migrations/meta/{0336_snapshot.json => 0342_snapshot.json} (98%) create mode 100644 packages/db/src/project-skill-sources-migration.test.ts create mode 100644 server/src/__tests__/project-skill-setup-identity.test.ts create mode 100644 server/src/__tests__/project-skill-sources.test.ts create mode 100644 server/src/__tests__/skill-source-retry.test.ts create mode 100644 server/src/services/project-skill-source-bindings.ts create mode 100644 server/src/services/skill-access.ts create mode 100644 server/src/services/skill-source-import-budget.ts create mode 100644 server/src/services/skill-source-import-queue.ts create mode 100644 server/src/services/skill-source-refresh-budget.ts create mode 100644 server/src/services/skill-source-retry.ts create mode 100644 server/src/services/skill-source-sync.ts create mode 100644 tests/storybook-visual/project-repository-skills.spec.ts create mode 100644 ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx create mode 100644 ui/src/components/skill-studio/SkillSourceConnectionRepair.tsx create mode 100644 ui/src/components/skill-studio/SkillSourceSyncControls.tsx create mode 100644 ui/storybook/public/project-skills-evidence/automatic-attachment.png create mode 100644 ui/storybook/public/project-skills-evidence/connection-repair-mobile.jpg create mode 100644 ui/storybook/public/project-skills-evidence/execution-master-final-marker.txt create mode 100644 ui/storybook/public/project-skills-evidence/execution-review-cli-marker.txt create mode 100644 ui/storybook/public/project-skills-evidence/execution-v1-marker.txt create mode 100644 ui/storybook/public/project-skills-evidence/execution-v2-marker.txt create mode 100644 ui/storybook/public/project-skills-evidence/index.html create mode 100644 ui/storybook/public/project-skills-evidence/repair-conflict-mobile.jpg create mode 100644 ui/storybook/public/project-skills-evidence/routine-execution-cli.jpg create mode 100644 ui/storybook/public/project-skills-evidence/routine-execution-final.jpg create mode 100644 ui/storybook/public/project-skills-evidence/routine-execution-reviewed.jpg create mode 100644 ui/storybook/public/project-skills-evidence/routine-execution.jpg create mode 100644 ui/storybook/public/project-skills-evidence/sources-desktop.jpg create mode 100644 ui/storybook/public/project-skills-evidence/versions.json create mode 100644 ui/storybook/stories/skills/project-repository-skills/README.md create mode 100644 ui/storybook/stories/skills/project-repository-skills/components.stories.tsx create mode 100644 ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx create mode 100644 ui/storybook/support/projectRepositorySkills.ts diff --git a/doc/DATABASE.md b/doc/DATABASE.md index a580717f0b8..f92ec1d1b45 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -756,6 +756,37 @@ register before writing, or they can remain unseen until the next startup or another notification. Tests should verify both the producer and its outer transaction boundary. +### Project repository skill synchronization + +`project_skill_sources` stores durable workspace import jobs and originating project +access provenance. Active bindings track their repository/ref; inactive bindings retain +access to installed versions. Project deletion removes bindings, but retained skills +with `access_source_id` stay restricted when no originating project remains. +Removing a project source hides tracking using `company_skill_sources.removed_at` +while retaining its access identity for editable copies and forks. Bindings become +inactive and refresh leases are invalidated transactionally. Reattachment preserves +removal; explicit adoption creates or joins an active source with a separate identity. +Migration 0341 limits repository/ref uniqueness to active sources. Removed sources +retain their original project audience; editable forks capture independent retained +provenance so later attachments cannot expose their local edits. Membership and +project visibility are still evaluated dynamically. + +`company_skill_sources` stores automatic-update and future-import preferences, setup +user/agent and connection/grant IDs, next-check time, failure count and existing refresh +leases. Existing sources default to automatic updates with their selection preserved. +Credential values remain in the connection secret system. No project membership lists +are copied into skills. Background publication uses normal immutable versions and +transactional activity attribution. Setup agent and connection identifiers remain after +those identities are deleted. Refresh then requires explicit repair; deletion cannot +silently substitute board authority or another connection. Execution snapshots keep +only the delivered skill keys and their immutable version IDs. New executions resolve +the latest installed versions without an upstream fetch. + +Project-derived sources retain `HEAD` for an unspecified repository ref. It follows +GitHub's current default branch. An explicitly configured branch has a separate +source identity even if it currently resolves to that default branch. API-key +agents without an authenticated run may attach repositories; their durable import +jobs have no setup person and require adoption by an authorized person. ### Task workspace records (2026-10-09) @@ -800,3 +831,16 @@ source captures leave no new records. Worktree realization uses a verified check as its Git command directory, including when metadata sits inside another repository. No migration scans or copies existing agent home directories. Keep admitted native descriptors and recovery references until compatible controllers finish their persistence obligations. See [the architecture rollout contract](plans/2026-10-09-task-workspaces.md). + +Migration 0341 stores the last scanned Git tree and a durable automatic-refresh +work allowance. The allowance starts at 512 MiB, refills at 512 MiB/hour, and +reserves the existing 128 MiB upper bound before automatic downloads. Completed +small scans refund unused capacity; interrupted/failed work keeps its bounded +reservation. These internal fields do not appear in source responses. + +Automatic initial repository imports reserve durable company work credits before +provider reads: 512 MiB capacity, refilling 512 MiB per hour, with a 128 MiB +reservation per attempt. Failed downloads and package fallback keep the full +reservation. Successfully linked small imports refund unused measured work. +Completed bindings do not reset company credits; waiting jobs retry on refill. +The company_skill_import_budgets table stores no credentials or source contents. diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 3fa2691e3f3..568f9275673 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -2320,3 +2320,16 @@ companion broker profile and Slack approval. Do not add manager client credentia to a local `.env` or an open-source deployment. See [Slack setup and Cloud qualification](connections/slack-setup.md) for the gates, recovery behavior, and production-component stories. + +### Project repository skills + +New GitHub project workspace attachments enqueue imports transactionally. A bounded +server worker drains imports and checks enabled sources every five minutes. Shutdown +cancels owned scans and releases their leases. Sources can be refreshed manually or +paused through Skills → Sources. Legacy repositories appear there without being +imported until the user chooses Import skills. + +Storybook: **Skills → Project repository skills**. Its production UI journeys use +in-memory provider and execution fixtures. Live GitHub refresh and execution need +separate acceptance evidence. Run the focused browser suite against built Storybook, +or set `STORYBOOK_TEST_BASE_URL` to an owned Storybook dev server. diff --git a/doc/PRODUCT.md b/doc/PRODUCT.md index 6d408844d3c..f6cbbb3a35f 100644 --- a/doc/PRODUCT.md +++ b/doc/PRODUCT.md @@ -100,16 +100,33 @@ The governing rule is: **skill permissions are opt-in restrictions, not opt-in c Core Paperclip owns the skill runtime, company-boundary enforcement, policy evaluation contract, API denials, validation, path containment, secret redaction, and activity logging. Those safety invariants cannot be disabled by policy. Open-by-default skill work never authorizes arbitrary host-path reads, unsafe executable content, or policy edits: local imports and scans must stay within Paperclip-known workspace or managed-skill roots, remote sources must resolve to validated immutable content, and platform safety denials must stay distinct from optional administrative restrictions. Paperclip EE may provide detailed administration for per-agent, per-role, per-action, per-source, and protected-skill rules, but EE is not required to use skills and is not an enforcement boundary. Without EE, companies remain open by default and any already-configured restrictions continue to be enforced by core. -GitHub repositories are first-class sources within Skills. People import selected -skills through their existing GitHub connection and manually refresh installed -snapshots. The source records repository, branch, path, and installed commit; -the connection supplies caller-authorized access. Originals remain viewable and -testable, with independent editable copies. New upstream skills require selection, -and deselection or upstream removal retains installed content. Disconnecting removes -the source after an explicit choice: keep independent editable copies in My Skills, -or delete its imported skills. Copies preserve agent assignments; skills still -used by agents must be detached before deletion. Already-disconnected sources -can be removed through the same flow. +GitHub repositories attached to new projects automatically become skill sources. +Paperclip imports their skills and checks the configured branch every five minutes, +using the setup person's authorized GitHub connection. New skills import automatically +unless excluded. Existing projects can adopt their repositories from Skills → Sources; +existing manually imported sources preserve their selections and gain automatic updates. +Refresh remains available, and operators can pause automatic updates. +If a tracked repository grows beyond the supported package size, its installed +repository skills and shared dependencies remain available until a safe refresh +can succeed. Initial oversized imports can use self-contained skill packages; +packages with omitted dependencies are invalid and are not installed. + +Project-derived skills inherit current project access. Access through any originating +project is sufficient, including the responsible-user restrictions on agent execution. +Existing explicit company imports retain their visibility. Editable copies inherit +project restrictions; private content cannot be published or exported implicitly. +Detachment retains installed content and access provenance. Deleting its last project +never makes a skill company-visible. + +Task and routine skill mentions remain stable references to the latest installed version. +Each execution records the immutable versions it uses; resuming keeps those versions, +while a new execution resolves latest again. Upstream failures and removals preserve +installed snapshots. GitHub refresh does not delay task execution. + +Disconnecting removes a repository from Sources after an explicit choice: keep +independent editable copies in My Skills, or delete its imported skills. Kept +copies and derived forks retain their project access restrictions. Copies preserve +agent assignments; skills used by agents must be detached before deletion. An explicit restricted policy may deny selected operations or switch to a default-deny preset with explicit allow rules. Core exposes a stable versioned policy API so EE and other administrative clients configure and simulate the same evaluator used by skill mutation routes. Core Skill Studio only needs to perform normal skill work, explain an explicit denial, and point administrators to EE when its richer policy UI is available; it must not recreate a partial enterprise permission editor. diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index f32064cb813..4ef7d3e1702 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -2016,9 +2016,11 @@ unavailable. Preserve current ownership and newer-work fences. See reduced-motion-aware animations and shows indeterminate progress while saving complete packages, without inventing completion percentages. -- Sources and entries are company-scoped. Reuse GitHub connection grants and credential - refresh; every provider read authorizes the current caller. Saved connection IDs do - not confer access to another user's token. Agent reads use managed run identities. +- Sources and entries are company-scoped. Discovery and connection changes authorize + the current caller. Automatic and manual source refresh use the stored setup person's + connection and grant, revalidating their access and refreshing credentials on each + check. Saved connection IDs do not independently grant library visibility. Agent + setup derives the responsible person from the authenticated run identity. - The import picker searches the deduplicated union of repositories from all connections the caller can access, automatically choosing an authorized connection. Adding accounts uses the standard GitHub setup in Apps and retains the import draft. Repository URLs @@ -2079,7 +2081,7 @@ unavailable. Preserve current ownership and newer-work fences. See and history. New key collisions cannot overwrite unrelated skills. Only changed package bytes or executable modes create versions. Library reads, tests, and subsequent unpinned runs use local installed snapshots; pinned/active runs retain their versions. -- Additions require reviewed selection, with new candidates checked by default and +- Manually selected sources retain reviewed selection, with new candidates checked by default and explicit folder exclusions retained. Upstream deletion keeps installed content with removed status; moves are removal plus discovery. Deselect retains content and assignments. Originals are read-only, with the existing independent fork workflow. @@ -2096,8 +2098,38 @@ unavailable. Preserve current ownership and newer-work fences. See resolve missing repository IDs and full snapshots on successful refresh. Exclude bundled, catalog, local/project, skills.sh, and unsupported hosts. Existing GitHub import and update endpoints delegate to sources while retaining response shapes. -- GitHub.com, manual refresh only. No upstream editing, polling, webhook sync, commits, - or pull-request creation in this milestone. +- GitHub.com project repository attachments enqueue durable skill-source import jobs in + the project mutation transaction. Skill import completes asynchronously and cannot + fail project creation. Configured refs use the existing project workspace precedence; + absent refs track the default branch. Sources without skills remain tracked. +- Enabled sources check upstream every five minutes; unchanged commits avoid downloads. + Manual Refresh uses the same serialized publication pipeline. Automatic updates may + be paused independently of selection. Project sources select future additions unless + excluded; existing manually imported sources retain their selections. Legacy projects + adopt through an explicit company-scoped, project-authorized endpoint, never a GET. +- Repository size fallback must not discard installed shared dependencies or + publish packages with detected omitted dependencies. Retain affected versions + and report the size problem. Self-contained packages can still update through + fallback. Project sources retry complete repository packaging and return to it + when supported; selections and exclusions remain unchanged. +- Unattended refresh stores setup identity and connection/grant identifiers, never token + values. Current membership, connection authorization and skill policy are revalidated. + Unrecoverable legacy setup identities require explicit connection repair. Retries are + bounded and backed off; leases survive restarts and shutdown cancels owned downloads. +- Project-derived skill visibility is the union of current originating project audiences. + Core uses project authorization, including agent/responsible-user intersection, for + discovery, reads, mutations, files, versions, tests and runtime delivery. Unauthorized + IDs return not found. Company imports retain their existing visibility. Forks preserve + project provenance. Unsupported public publication or portable export is rejected. +- Detachment retains installed content and project access provenance while stopping that + project's tracking. Ref replacement never rewrites existing references. An orphaned + project source remains restricted after its last originating project is deleted. +- Executions resolve unpinned references from local immutable versions, persist the + resolved inventory, and reuse it on resume after rechecking access. Explicit unavailable + references fail actionably rather than silently omitting instructions. Background + GitHub checks never delay execution. Existing version pins remain supported. +- Upstream editing, push webhooks, commit/PR creation and advanced reference pin UI + remain outside this release. ## Public assistant connection (opt-in) @@ -2188,6 +2220,17 @@ See [the layer architecture](plans/2026-10-09-task-workspaces.md) for resolution Companies may configure an optional shared API connection and model for short acknowledgements of accepted human turns. Fast responses use direct SDK inference alongside normal agent work, with a three-second admission deadline, bounded prompts/output, current authorization and independent `fast_response` accounting. They are server-marked platform receipts attributed to the agent, never completion, interaction or recovery evidence. Company-sponsored unlinked external senders are explicit; linked-user permission denials are never bypassed. See [fast-responses.md](fast-responses.md) for provider, delivery, history and endpoint contracts. +Project-derived source removal retains dynamic project access for kept editable +copies and forks. A removed source is absent from Sources and cannot refresh; +ordinary repository attachment does not restart it. Explicit project adoption can +restart tracking without widening the retained content’s access. + +Codex execution homes are scoped to the agent’s authorized skill inventory, +including its immutable source versions and an empty inventory. Revocation and +version changes select a separate home and session store while retaining the +selected model account’s credential source. Repository skill auto-update trusts +its upstream authors; operators can pause updates to review changes manually. + ### Task collection creation visibility (2026-10-13) Tasks and project collections default to hiding agent/system-created tasks and @@ -2207,3 +2250,32 @@ read-only activity do not admit scheduled work. Inbox discovery/Blocked preserve participated automation and hide unrelated automation by default. Agent profile history, routine history, and task-detail subtasks remain complete. API consumers that omit the optional creation filter retain their existing list behavior. + +Project-derived editable forks retain the project audience present when created. +Later repository attachments do not grant access to those edits. Re-adopting a +removed source uses a separate active tracking identity; retained copies keep +their original provenance. Project membership and visibility remain dynamic. + +Automatic skill checks compare tree identity as well as commit identity, so +empty commits do not download or rescan content. Automatic scan work uses a +durable per-source 512 MiB allowance with 512 MiB/hour refill; installed versions +remain usable when it is temporarily limited, and authorized manual Refresh +remains available. Initial imports authorize skills.import before provider work. +At most 20 jobs per project and 100 per company enter the active initial queue; +additional repository bindings remain durable and are admitted automatically +when capacity frees. Project workspace create/update/delete use the existing +workspace-write authorization evaluator. Policy-denied initial jobs await explicit +adoption/repair rather than repeatedly scanning. + +Agent-created project sources may reuse only the GitHub connection and grant +already authorized in the persisted active run identity. The responsible person +is recorded for attribution; that identity alone does not permit searching other +personal connections. Without a selected run grant, Sources requires an explicit +human connection repair before background import or update. + +Automatic initial repository imports reserve durable company work credits before +provider reads: 512 MiB capacity, refilling 512 MiB per hour, with a 128 MiB +reservation per attempt. Failed downloads and package fallback keep the full +reservation. Successfully linked small imports refund unused measured work. +Completed bindings do not reset company credits; waiting jobs retry on refill. +The company_skill_import_budgets table stores no credentials or source contents. diff --git a/doc/plans/2026-10-12-project-repository-skills.md b/doc/plans/2026-10-12-project-repository-skills.md new file mode 100644 index 00000000000..2fb05771be3 --- /dev/null +++ b/doc/plans/2026-10-12-project-repository-skills.md @@ -0,0 +1,201 @@ +# Project repository skills + +## Finish line + +Deliver automatic imports and five-minute refresh, project-derived visibility, +latest-version execution with immutable execution snapshots, legacy adoption, +production UI and Storybook coverage. Completion requires passing repository +checks, a live GitHub-to-routine update journey and a reviewable PR with evidence. + +## Decisions + +- Track the configured project branch, defaulting to GitHub's default branch. +- Import future skills unless excluded. Existing manually imported sources keep selections. +- Refresh using the setup person's existing authorized GitHub connection. +- Access through any originating project is sufficient; evaluate current project ACLs. +- Retain installed versions and project provenance after detach or upstream removal. +- New executions use latest installed versions; resumed executions keep their snapshot. +- Legacy projects adopt explicitly. Existing imported sources update automatically. +- Advanced reference pin UI and GitHub push webhooks are deferred. + +## Current qualification — 2026-10-13 UTC + +Implementation and live acceptance passed, including upstream updates, private +access/recovery matrices, eleven rebuilt browser journeys, responsive inspection +and a final real v2 execution after master reconciliation. All standard local test +phases completed across documented retries. The final safe-fallback correction +passed 89 pinned Node 24 cases and server typecheck/build. Project fallback retains +shared dependencies, permits safe package updates and restores complete packaging +when the repository fits. Final review/check results and authorized merge state +are recorded on PR #16066; only its exact green head may be merged. + +GitHub CLI authentication was restored by the user. Local and browser-published +histories were reconciled with an unchanged, identical tree. Final-head checks +must pass before merging. Merge is already authorized; no further merge +permission is needed. + +The temporary reconciliation checkout and obsolete previews have been stopped +and removed. Final local acceptance data and evidence are retained. See the +[acceptance report](2026-10-12-project-skills-acceptance.md) for current outcomes +and limits. The process notes below are historical and do not supersede this +checkpoint. + +## Historical progress + +- Repository implementation and verification paths inspected; worktree started clean. +- Persistence, attachment jobs, background checks, manual refresh, project access, runtime snapshots and Sources UI implemented; integration review remains. +- Initial preview migration was `0336_lovely_bloodstorm.sql`; master reconciliation replaces preview migrations with replay-safe generated `0341_abnormal_bucky.sql`. +- Focused source persistence tests passed (13 existing cases); new project source tests passed (3 lifecycle/access cases). +- Source API/UI regression tests passed: 69 tests. Runtime-skill regression suite also passed. +- UI/server direct TypeScript checks passed after building workspace dependencies; repeat after latest changes. +- Token gates passed. Storybook build passed. +- Storybook Playwright journeys passed: legacy adoption/navigation, automatic additions, failure/retry, preference keyboard behavior and mobile overflow (5 tests). +- Manual Sources review completed in IAB; fixed cramped intermediate-width action layout. Project creation and routine Storybook walkthroughs remain. +- GitHub account `cryppadotta` authenticated. Claude and Codex CLI sessions authenticated; no model API key in environment. Live test must use an authorized existing CLI session or connection. +- Live GitHub acceptance, expanded permission/worker/runtime coverage, required full checks and PR handoff have not run. + +## Current process/evidence pointers + +- Branch: `codex/project-repository-skills`; checkout `/Users/dotta/.codex/worktrees/4631/paperclip`. +- Owned Storybook: `http://127.0.0.1:6187`, exec session 14926. Port 6007 belongs to another process and was not changed. +- IAB tab 1 shows the full Sources journey; browser binding 4. Preserve/reuse the binding when continuing. +- Runner dependency build completed; exec session 59507 was its build handle. +- Logs in `/private/tmp/project-skills-*.log`; browser screenshots under `tests/storybook-visual/test-results/` (ignored). +- Full feature remains the finish line. Next: expand authorization/runtime tests, inspect project/routine journeys, launch isolated live instance and complete GitHub update execution acceptance. + +## Verification + +Source/worker lifecycle and failure tests; authenticated human/agent access matrix; +runtime latest/resume tests; Storybook browser journeys and responsive review; +live disposable GitHub repository and supported runner; token/map gates, Storybook +build, recursive typecheck, Vitest and production build. Record actual results here. + +## Live acceptance progress (2026-10-12) + +- Created the project and attached the disposable private GitHub repository through + the production browser UI. The worker imported `acceptance-marker` without an + import action. No agent assignment was added automatically. +- Created a routine through the UI with a stable skill mention. The real + `codex_local` runner (`gpt-6.1-sol`, existing ChatGPT CLI login) completed it and + wrote the exact first marker from the installed skill reference file. +- Pushed an updated marker and a new skill. The production worker imported both + at 23:21:03 UTC, without Refresh, about five minutes after initial import. +- Ran the unchanged routine through the UI again. The runner wrote the exact + updated marker. The first successful run retained version + `1ccdfa45-4a82-44ad-96cb-9ffa6e20fc64`; the second selected + `2bd9801c-1b96-417b-b081-2867e65afdcf`. +- A real agent bearer key could not list/read the private skill before project + membership (404), and could after membership (200). Removing membership revoked + access. Granting membership through a second private project restored access; + its source response exposed only that accessible project's name. Both projects + reused one source and the original skill identity. +- Restarted the owned instance with its retained data and latest source changes. + Doctor passed for PID 40476 at `http://127.0.0.1:3110`; source links, installed + skill identities and selections persisted. +- Manual refresh succeeded. Disabling the disposable GitHub connection produced + a clear source error while retaining both installed skills. The connection was + restored; the browser connection-repair journey is in progress. +- Initial provider setup attempted an unsupported model (`gpt-5.4`); corrected + the disposable agent to the supported model before claiming live execution. + Agent provisioning was an API fixture. Project/routine creation and execution + used the real UI. No synthetic model response was used. + +Evidence directory: `/private/tmp/paperclip-project-skills-proof-BGunNG`. +Non-secret records include `fixture.json`, `execution-v1.json`, +`execution-v2.json`, marker files, `automatic-update.json`, +`private-agent-access.json`, `private-union-access.json`, `connection-failure.json` +and `execution-v2.jpg`. The separate test key file is credential material and must +not be published or attached. + +## Latest verification and remaining work + +- Live legacy adoption reused the existing source and skill IDs. Listing the older + repository did not import it. The real Sources action performed adoption. +- Seeded disposable human identities and real bearer authentication passed direct + skill/file/version/source checks. Access through either project worked. Removing + membership revoked access. Agent responsible-user mismatch failed closed. + This is API identity proof, not a multi-person browser sign-in test. +- Linking the repository through an open legacy project made its skills visible. + Changing that project to private revoked access for the unrelated company member. +- Real UI connection repair persisted after reload and kept installed selections. +- Current instance restarted at http://127.0.0.1:3109 (owned PID 73371). Startup + applied migration 0337. Doctor passed. The unchanged routine produced the exact + v2 marker again in successful run f003baa3-37d7-4203-bd4d-a0c4b45171ca. + Its snapshot includes four selected immutable versions and excludes the new, + unassigned project skill. Older execution version evidence remains unchanged. +- Recursive typecheck, production build, Storybook build, maps and token gates + passed. Six browser journeys passed against built Storybook (no retries). +- Latest focused server/UI check passed 94 tests. Worker/startup check passed 44. + The isolated recovery test passed. A GitHub review fixture was corrected to admit + a new event before checking changed configuration; that focused test passed. +- Full suite is running in exec session 45645, log + /private/tmp/project-skills-final-full-suite.log. Observe this exact handle. +- Manual mobile review found cramped provenance names. Source headings now use + the full narrow-screen width, with actions below. Token/UI checks are rerunning. + Desktop, intermediate and mobile real-app light captures and mobile dark capture + are in the evidence directory. Finish both-theme Storybook and recovery-dialog + review before final handoff. +- Still required: final full-suite result, remaining requirements audit, final UI + build/browser verification, sanitized inspectable acceptance report, commits, + rebase and template-compliant PR/review handoff. +- Owned current server session 94907; development Storybook 14926; built static + Storybook 34961 (6188). Preserve ownership and use the stop helper for cleanup. + +## Rebased checkpoint — 2026-10-12 + +- Implementation is in four logical commits on `codex/project-repository-skills`, + rebased onto master `4b958ecbd8`. Current committed head is `afc5f22a0c`. +- Master added the downstream Storybook catalog. The new suites now live in the + Skills feature home, with explicit stable IDs preserving every existing URL. + Shared simulation lives in `ui/storybook/support/projectRepositorySkills.ts`. + Catalog structural checks passed (225 files / 2,235 stories) and nine catalog + tests passed. Existing source dependency records preserve their classifications. +- Post-rebase recursive typecheck, production build, Storybook build, map/token + gates and all six built-browser journeys passed. No Linux visual baseline was + compared or updated. +- The earlier full-suite process was stopped before rebase. The authoritative + post-rebase full suite is exec session 12928, log + `/private/tmp/project-skills-rebased-full-suite.log`. It is live; do not restart + it because output is quiet. +- The owned instance restarted at `http://127.0.0.1:3110`, PID 71855, server + session 62515, and Doctor proved committed head `afc5f22a0c` with ready UI/API. + Its retained source, skill identities, routine and execution history persisted. +- Manual mobile connection repair passed via keyboard. A missing focus return + was observed after saving; the UI now returns to the source actions button. + Its eleven focused UI tests and UI typecheck passed. The updated Storybook + build is session 73545 and must be followed by the browser focus assertion. +- Keep the full objective active. Still required: final suite result, complete + requirements audit, remaining theme/state review, acceptance evidence handoff, + template-compliant PR and review/check loops. No PR has been opened yet. + +## Review fixes and evidence checkpoint + +- PR #16066 is open in draft and attached to the Codex task. Initial CI passed; + Greptile requested fixes for API-key repository creation, default-branch tracking, + local file version freshness, and missing screenshots. +- API-key agents can attach repositories without an authenticated run. Their jobs + retain no invented setup person. Default-branch bindings preserve HEAD, separate + from fixed branches. Fresh unpinned local executions capture changed installed + files; resumed/pinned executions keep their immutable selections. +- A local-import path was proved to discard private snapshot provenance. It now + rejects known private snapshots/materialized copies as company-wide imports, + with an opaque not-found response for unauthorized readers. +- All 27 focused source/runtime tests passed after these fixes. Six browser cases + passed with real emulated touch taps, keyboard controls and repair focus return. +- The first post-rebase full run finished: 17,961 passed, six failed. Two failures + used pre-review module implementations alongside added tests. Four other cases + timed out or lost an HTTP/database connection and are being rerun in isolation. + Final qualification must use a fresh full run on the frozen reviewed code. +- The approved initial Storybook preview published through the existing CODEOWNER + workflow. Its immutable build is 38329548539-1 for commit 3e82dada92. Publish the + reviewed head before handoff. Acceptance screenshots and minimized execution + receipts are now in ui/storybook/public/project-skills-evidence/ for the existing + Storybook artifact workflow. No credential or identity key files are included. + +## Reconciliation checkpoint — 2026-10-12 evening + +Master integration preserves the durable task-workspace changes alongside project skill authorization and execution snapshots. The regenerated migration follows master's 0338 and its real database replay regression passed. Recursive typecheck and production build passed in the temporary reconciliation worktree. Ten focused Storybook journeys passed, including project creation, second repository attachment and immutable simulated routine revisions. Final built-artifact checks, full-suite qualification, updated PR review and a current implementation acceptance run remain. + +The reviewed-head full run ended with 17,687 tests passing, two failing, 383 skipped and 18 failed files. Database startup errors identify host shared-memory exhaustion; public MCP also timed out. This is a failed run, not completion evidence. The owned acceptance server was stopped after recording successful reviewed-head run e2cbfc2b-aa07-4fa0-b240-82033d6d1035, and its data/evidence remain available. + +The user authorized merging once final PR checks pass. Do not merge an incomplete or conflicting head. diff --git a/doc/plans/2026-10-12-project-skills-acceptance.md b/doc/plans/2026-10-12-project-skills-acceptance.md new file mode 100644 index 00000000000..74c499e61ce --- /dev/null +++ b/doc/plans/2026-10-12-project-skills-acceptance.md @@ -0,0 +1,517 @@ +# Project repository skills: acceptance evidence + +Live GitHub updates, immutable routine executions, access/recovery checks, +production-component browser journeys, and manual responsive review passed. +Local standard test phases were completed across documented environment retries. +Final review/check results and merge state are recorded on PR #16066; merge is +gated on its exact head. This report separates real outcomes from test fixtures. + +## Environment and artifacts + +Implementation checkout: `~/.codex/worktrees/4631/paperclip`, branch +`codex/project-repository-skills`, PR [#16066](https://github.com/paperclipai/paperclip/pull/16066). +The isolated instance uses embedded PostgreSQL and a retained disposable data +directory outside the checkout. Doctor verified the served source revision and +loopback port ownership before the live journeys. No production company data was +used. The test GitHub repository is private and contains only generated skill +fixtures. The supported live adapter is `codex_local`, using model `gpt-6.1-sol` +through the existing authenticated Codex subscription. + +The tracked [evidence bundle](../../ui/storybook/public/project-skills-evidence/) +contains the original four captures and a final post-reconciliation capture and an explicit, minimized +[version receipt](../../ui/storybook/public/project-skills-evidence/versions.json). +It is included in the existing Storybook build artifact and branch publication. +Credential files, full provider logs, human authentication keys, and connection +secrets are excluded. This direct Codex task has no driving Paperclip issue or +injected artifact-upload context; the PR and Storybook artifact provide the +review access path. + +## Live GitHub → import → execution + +1. Provisioned an empty company, one authorized GitHub connection and a supported + test agent through normal APIs. This is fixture setup, not onboarding proof. +2. Created a project and attached its repository through the real board UI. + The background worker imported `acceptance-marker` without a second skills + setup step. Import did not assign the skill to every agent. +3. Created a routine through the real UI and selected the stable slash mention. + The runner read the installed skill and reference, then wrote the exact + `project-skill-v1` marker in the task workspace. +4. Pushed a changed skill and a new skill to the configured branch. The production + worker imported both at **23:21:03 UTC**, without Refresh, about five minutes + after initial import. +5. Ran the unchanged routine through the UI again. The runner wrote the exact + `project-skill-v2` marker. The earlier execution still identified its original + immutable version. +6. Restarted the owned instance with retained data. A further real execution + succeeded with v2. Its snapshot contained only four delivered immutable + versions; the newly imported, unassigned skill was absent. + +| Execution | Immutable project skill version | Exact output | +| --- | --- | --- | +| `2db73573-eeaf-420e-aad8-898a87377342` | `1ccdfa45-4a82-44ad-96cb-9ffa6e20fc64` | `project-skill-v1` | +| `ae685356-02a9-4369-9bb3-b76bb112bcce` | `2bd9801c-1b96-417b-b081-2867e65afdcf` | `project-skill-v2` | +| `f003baa3-37d7-4203-bd4d-a0c4b45171ca` | `2bd9801c-1b96-417b-b081-2867e65afdcf` | `project-skill-v2` | +| `e2cbfc2b-aa07-4fa0-b240-82033d6d1035` | `2bd9801c-1b96-417b-b081-2867e65afdcf` | `project-skill-v2` | +| `b91818ac-9e9a-4a98-bd6b-63c0a58e4a1a` | `2bd9801c-1b96-417b-b081-2867e65afdcf` | `project-skill-v2` | +| `83f6f277-5a4a-4814-ba55-8a7ed4a517e1` | `2bd9801c-1b96-417b-b081-2867e65afdcf` | `project-skill-v2` | + +The earlier reviewed real execution passed on implementation +`614a2d1d72892128c7fc949d2485308756e7c687`. It delivered four selected immutable +versions, excluded the unassigned addition, and preserved the first run's v1 +snapshot. The uploaded output was exactly 17 bytes, with the v2 SHA-256 hash in +the receipt. The model was `gpt-6.1-sol`; its successful heartbeat execution is +`b91818ac-9e9a-4a98-bd6b-63c0a58e4a1a`. + +After master reconciliation, another real execution passed on +`cea9e474ba462ed9385ce2f8acb58c245585b310`. The unchanged routine completed +through the real API and supported runner; the completed task was inspected in +the browser. It wrote a fresh 17-byte workspace file matching v2 exactly, delivered +the same four immutable versions, and left the original execution on v1. Startup +checked the unchanged upstream source successfully at 11:45:25 UTC. This output +is a verified workspace file; the earlier reviewed execution also proved upload. +The new receipt and capture distinguish those two evidence paths. + +Initial upstream commit: `d7e95f01903678531981ce684e721191ecb0a7da`. +Updated upstream commit: `f4b35ea0082150bda320cda7b3c0777d263bf430`. +The marker files and their SHA-256 hashes are in the receipt. These runs used +real model responses. An initial unsupported model selection failed; the agent +was corrected to the supported model before recording successful execution. + +## Recovery, adoption, and access + +- **Manual refresh:** the real Sources action completed without replacing skill + identities or selections. +- **Connection failure:** disabling the disposable connection produced an + actionable source error while retaining both installed skills. Re-enabling it + and saving an authorized connection through the real repair dialog recovered + updates. Reload preserved the result. +- **Legacy adoption:** an existing workspace was seeded without a binding to + represent pre-feature data. Listing it did not import anything. The real + **Import skills** action reused the existing repository source and skill IDs. +- **Private agent access:** a real bearer key could not list/read the private + skill before project membership. Membership granted access; removal revoked + it. Membership in another qualifying private project restored access. +- **Human access:** separate disposable users and real hashed bearer credentials + passed skill, file, version, and source reads only through their qualifying + projects. Responses omitted inaccessible project names. An unrelated company + member received not-found responses. Revocation took effect on subsequent reads. +- **Responsible person:** an agent with project access could not read on behalf + of an unrelated person. The same agent could read on behalf of an authorized + project member. +- **Open/private transition:** linking through an open project made the shared + source visible. Changing that project to private revoked the unrelated + company member's access. + +The multi-person evidence uses seeded identities followed by production bearer +middleware and APIs. It does not claim separate browser sign-in journeys. The +private GitHub repository requires the authorized owner's access for inspection. + +## Automated proof + +The final focused project-source suite passed **12 cases**. The source and runtime +suites passed **16 cases**. The replay-safe migration regression passed with a real +embedded database, retaining installed content, private provenance, scheduling, +and setup identities when renumbered SQL runs again. + +The service and authorization suites cover transactional enqueue/rollback, +legacy edits without adoption, empty repositories, unchanged commits, additions, +exclusions, invalid siblings, source deduplication, ref changes, detach/delete, +lease competition/expiry, restart recovery, graceful shutdown, backoff/rate limits, +revoked setup access, and explicit connection repair. They also cover private +reads, forks and publication, responsible-person restrictions, immutable resume, +explicit pins, inaccessible references, and fresh local file snapshots. Known +project-derived local snapshots/materialized copies cannot become company-wide +imports. Default-branch HEAD and explicit branches retain distinct identities. +OAuth expiry and larger failure matrices use deterministic provider boundaries; +no live OAuth-expiry claim is made. + +**Eleven browser journeys passed against the built Storybook artifact** on the +final implementation. They cover automatic import after project creation and +second repository attachment, empty-source guidance, legacy adoption/navigation, +background additions, refresh failure/retry, keyboard preferences, mobile touch, +connection repair/focus, repair after a concurrent update, and latest routine +execution with immutable earlier versions. Stories mount shipped pages and +components with simulated provider/import/execution data; they do not replace +the live acceptance above. + +Recursive typecheck, production build, Storybook build, token gates, map checks, +and catalog checks passed after master reconciliation. The final fallback follow-up +also passed server typecheck/build and **89 pinned Node 24 cases**: scanner/source, +project worker, and both workspace authorization suites. Eleven built-browser +journeys passed again after master reconciliation. The signoff retry passed ten +browser/API cases across two runs. + +The standard `pnpm test:run` phases on source d8ab984 were completed across retries: + +| Phase | Passed files | Passed cases | +| --- | ---: | ---: | +| General server | 900 | 18,429 | +| UI | 729 | 8,181 | +| CLI | 66 | 527 | +| Shared | 89 | 908 | +| Skills catalog | 5 | 20 | +| Adapter utilities | 73 | 1,736 | +| Other workspaces | 130 | 2,079 | +| Database | 51 | 194 | +| Serialized server | 158 | 3,113 | + +This is complete phase coverage, not a claim that one uninterrupted monolithic +command exited zero. The serialized run passed 156 files, then hit a 15-second +route-test timeout; the failed file and the last unrun file passed all 21 cases in +isolation. The final five-suite run passed all 89 cases on source +`40bafbb7a2f7753ac48765081bd2583f611cf5f2`, preserving assertions and deadlines. +Full final-head CI covers the reconciled master and subsequent corrections; +its authoritative results are linked from the PR. + +GitHub CLI authentication was restored by the user. Earlier browser publication +and local source trees were identical and their histories were reconciled without +content changes. No host credentials or production data were mounted in the Linux +verifier. Review findings were fixed with regressions and resolved through the +[check-pr workflow](../../.agents/skills/check-pr/SKILL.md). + +Earlier local full runs failed. macOS runs encountered database startup/hook +failures from shared-memory exhaustion and a public-MCP timeout. The first Linux +setup omitted generated SDK builds; the second lacked standard test utilities +and was stopped after that diagnosis. Those attempts are not passing evidence. +Two pre-existing login tests also assumed an x64 host. Their intended native +and legacy platforms are now explicit, and all 13 login cases passed on ARM64. +The subsequent 2 GB heap run exited unsuccessfully: 18,422 tests passed, +76 skipped, and one worker aborted from JavaScript heap exhaustion in the +existing setup-token route suite. Its 30 cases passed with an explicit 4 GB +heap. With the 4 GB heap, all 900 server files passed: 18,429 tests passed and +76 skipped. The UI group then reported 8179 passed and one failure. The reset +reproduced in isolation; its editor now holds accepted selections until parent +data refreshes. Both focused UI suites passed all 38 cases. Subsequent phase completion and final focused verification are recorded above. +The final Linux run uses Node 24, built workspace dependencies, pinned Rust, +and the standard process, shell, SSH and JSON test tools in an isolated container. +No host credentials or production data are mounted. + +The ownership-checked acceptance cleanup passed: runtime, loopback port 3107, +and its embedded database stopped cleanly; disposable data and evidence remain +retained. All owned Storybook previews and the temporary reconciliation checkout +were removed after their captures and verification logs were preserved. + +## Experience review and limitations + +Manual real-app inspection covered mobile, intermediate and desktop in both +themes. Storybook inspection included long names, validation warnings, empty and +paused sources, retained removed skills, private project unions, and an 80-skill +list. The list had no horizontal overflow and its footer was keyboard reachable. +A cramped provenance layout, missing repair focus return, and blank simulated +routine section were found, fixed and covered by regression journeys. + +The final concurrent-repair story also passed manual inspection at 390px in dark +mode. The selected account remained checked after a 409, its error and footer +fit on screen, keyboard retry succeeded, and focus returned to source actions. +The shared task-defaults editor was also inspected after saving on mobile in dark +mode: the acknowledged selection remained visible and Save became disabled. +Sampling does not claim interaction coverage for all **27 new exports**. No Linux +screenshot baseline was compared or updated. + +The existing Storybook deployment workflow published commit 5869973 in run +38394544972, including the sixth real execution receipt and final task capture. +Subsequent fallback changes affect server code; shipped UI and story sources remain +identical to this published build: +[review guide](https://github.com/paperclipai/paperclip/blob/586997352198b2dbf025445706762e27d38e6764/ui/storybook/stories/skills/project-repository-skills/README.md), +[project creation](https://d1p6rlowie26tp.cloudfront.net/storybook/branches/codex-project-repository-skills-8ad0b8be51ae75bc/builds/38394544972-1/?path=/story/skills-project-repository-skills-02-journeys--create-project), +[repair retry](https://d1p6rlowie26tp.cloudfront.net/storybook/branches/codex-project-repository-skills-8ad0b8be51ae75bc/builds/38394544972-1/?path=/story/skills-project-repository-skills-02-journeys--repair-conflict). +Published deployment metadata and story IDs were verified over HTTP. The in-app +browser blocked CloudFront navigation, so hands-on inspection used the identical +local built artifact. No browser security warning was bypassed. + +## Inspectable captures + +Real isolated Sources page: + +![Real Sources page](../../ui/storybook/public/project-skills-evidence/sources-desktop.jpg) + +Shipped mobile repair dialog with Storybook fixtures: + +![Mobile connection repair](../../ui/storybook/public/project-skills-evidence/connection-repair-mobile.jpg) + +Real completed routine execution: + +![Real routine execution](../../ui/storybook/public/project-skills-evidence/routine-execution-final.jpg) + +## Review fixes + +Initial-import and scheduled-refresh queries exclude live leases before bounding +batches. A real database regression places ten leased records ahead of eligible +work and verifies import and refresh still proceed. Connection repair derives +its source from the current query and reloads the revision after a conflict while +retaining the chosen account. Runtime-test teardown drains tracked execution +writes before truncation, resolving a reproduced database teardown deadlock. + +![Mobile concurrent repair](../../ui/storybook/public/project-skills-evidence/repair-conflict-mobile.jpg) + +The final review identified an oversized-repository refresh that could drop shared +dependencies. Repository sources retain installed versions when fallback would omit shared +files; safe self-contained packages can still update. Initial and subsequent project fallback scans reject detected omitted +dependencies. Project sources automatically restore complete packaging when it fits. All 56 scanner +and source-service cases passed, including manual and scheduled oversized refresh, +retained shared/sibling files, unchanged versions, and recovery after size reduction. +The signoff fixture retry also passed ten real browser/API cases across two runs. + +Environment retries also preserved the original failures: a telemetry opt-out +variable conflicted with a mocked CLI test; Docker overlay storage affected probe +identity tests; a native temporary mount initially disallowed execution; and an +oversized stream fixture failed under that mount. The normal-storage cases and +all 56 probe cases on an executable native temporary filesystem passed. A later +host rerun used Node 26 and hit database/subprocess timeouts. The pinned Node 24 +run passed the unchanged audit deadline, project suites and authorization cases. +No assertions or timeout thresholds were relaxed to obtain these results. + +## Final source-removal integration + +Integrated master’s keep/delete choice with project provenance. Removed project +sources retain an internal access identity, invalidate refresh and attachment leases, +and stop tracking. Kept copies and forks remain dynamically restricted. Ordinary +reattachment retains removal; explicit adoption restarts tracking. The rebuilt Storybook passes all 13 interactive project-skill browser cases, including +kept-copy access wording and cancelled removal; the Sources UI/API suites pass 21 +cases. The initial Linux pass exposed two harness mismatches: source authorization +now precedes request validation, and repository skill instructions use a wrapper. +Both assertions retain their intended contract with corrected fixture setup/file +paths; the final service pass and current PR checks are recorded on PR #16066. + +## Final review corrections + +The security review’s per-revision approval recommendation conflicts with the +approved automatic/latest behavior. The response records repository-author trust, +per-refresh credential/access/policy revalidation and the manual-update option. +The scanner subsequently passed. Greptile then identified two concrete defects: +legacy adoption without a saved source ID did not restore removed tracking, and +Codex’s reused home could retain a revoked private skill link. The initial correction resolved the removed repository/ref; the final privacy +review superseded reuse with separate active tracking identity, described below. Codex launches against a +home scoped to its permitted immutable inventory, including an empty inventory, +with the selected credential source preserved. Three Codex launch/injection/sync +suites pass 32 cases; final service and real-provider results are recorded on the PR. + +Post-review verification passed 212 cases across 13 pinned Node 24 suites, +plus the full typecheck and build. A fresh real ACP routine execution (`PRO-7`, +`95d14fcf-5643-4d5f-9512-d160efcb2446`) delivered four pinned versions and +reported the v2 marker. The CLI probe exposed the host’s old Codex 0.153.4 +client: both requested models were rejected before model work. The repository +already pins Codex 0.160.0. With that client and the original `gpt-6.1-sol` model, +`PRO-9` succeeded in run `d94d7009-8787-4483-81e4-71ec0b512ed6`, marked its +task done, and independently verified the existing 17-byte v2 evidence file +without rewriting it. Its invocation used `skill-runtimes//`; +the selected account remained usable. The v1 execution still identified version +`1ccdfa45-4a82-44ad-96cb-9ffa6e20fc64`. Eight successful execution receipts +now distinguish newly written files from provider-reported or preserved outputs. +Failed probes are recorded separately. The disposable agent’s original settings +were restored and the superseded probe task was cancelled. + +The live walkthrough also reproduced a withdrawn AI connection request still +showing an active “Fix connection” action after a successful task. A failing +regression test captured it. Withdrawn requests now use the existing terminal +card renderer; 70 interaction/Sources UI/API cases pass. A production-component +Storybook state and a narrow light-theme browser case preserve this recovery state. + +The final compatibility review also found provider settings skipped for generated +Codex homes. A launch-level regression failed for both default and explicit +credential homes. Provider settings now apply to generated skill homes while +caller-owned homes remain untouched; the three Codex suites pass 33 cases. + +Final qualification passed 213 service/runtime cases on pinned Node 24, 70 +interaction/Sources UI/API cases, and 14 rebuilt Storybook browser journeys. +Typecheck, build, token gates, maps and catalog checks passed. The restarted +real app on source `c9a5897bb0843ee5ac89ee0bb9210ca864d6ceca` shows the +withdrawn request as inactive after reload, alongside the completed CLI result. +The updated CLI capture records this corrected recovery state. Final head checks +and merge status remain authoritative on the PR. + + +The final retained-content privacy correction separates tracking from provenance. +After removal, adoption creates or joins an active source; it never adds readers +to the removed source holding editable copies. Forks capture the originating +project set independently, so later open-project attachments cannot expose private +local edits. Membership, project visibility and deletion remain dynamic. Generated +migration 0341 makes source identity uniqueness apply only to active rows. The +regression matrix covers public re-adoption, direct private file/version denial, +private fork preservation, original-workspace adoption, and membership revocation. + +Final qualification of the retained-audience correction (`4f7df5e683`) passed +232 tests across 14 pinned Node 24 suites. One scanner subprocess deadline failed +under concurrent build/install load; the unchanged full batch passed on retry. +The 37 source/privacy cases also passed on macOS. Final typecheck and production +build passed after installing two test dependencies added by concurrent master. +Repository policy leaves lockfile publication to its scheduled refresh bot; no +lockfile change remains in this PR. The rebuilt artifact passes all 14 interactive +browser cases. Maps, token gates and catalog tests pass; the catalog inventories +227 files and 2,252 stories without claiming all were rendered. Application and +preview processes owned by this task were stopped with their evidence retained. +Current-head GitHub checks and review results are recorded on the PR. + +The final catalog review found stale immutable versions after installed catalog +bytes changed. A reference-only update regression failed before the fix. New +unpinned executions now compare installed catalog files, as they already did +for local files, without network fetching; unchanged content reuses its version +and pinned resumes preserve the old bytes. The runtime/catalog suites pass all +17 cases. This final correction changes no UI; the published Storybook build +38427815861 pins 6219538f75 and contains all eight successful execution receipts +and the two explicitly classified old-client failures. + +A fresh review then identified a removal/import race and stale account selection +in generated Codex homes. Both were reproduced before correction. Final source +linking locks the source before the binding, follows removal's lock order, and +rechecks removal and the binding lease. A losing import retries with independent +tracking; it never adds readers to retained edits. Generated homes now include +the selected subscription account and credential source in their digest. Switching +accounts uses a new home; same-account token refresh keeps the home. The regression +uses a regular credential file to model remote copy-back. The source/Codex suites +pass 71 cases, and recursive typecheck and production build pass. These are +deterministic race and launch tests; they do not claim another live model run. + +The final default-account follow-up reproduced a mismatch between host and +company credentials: hashing the host source while seeding the saved company +login could reuse a previous account's generated home. The digest now reads +the same prepared company or explicitly selected home used for seeding. The +default-company switch regression fails before correction and passes afterward. +All 33 Codex launch/injection/sync cases and recursive typecheck pass. The +source-removal correction passed fresh review; final CI/review results remain +recorded on the PR. No extra live-provider result is claimed for this correction. + +The final security scan identified API-key imports persisting a responsible user +as unattended refresh authority. Direct imports now share project attachments' +setup resolver: API keys retain attribution without a setup person; authenticated +runs derive the person from the persisted active run. Legacy recovery cannot +restore deliberately absent API-key authority and accepts agent attribution only +with a matching recorded run. With valid membership fixtures, both API-key +regressions fail on the previous route setup and pass after correction. All 101 +affected route/source/database cases pass, including scheduled repair and legacy +audit recovery. Recursive typecheck and production build also pass. Current-head +security and CI results are recorded on the PR. + +Final master reconciliation includes `da9af6741585aeb439b5d38af398eab841131d6e`. +Master added migration 0339, so the unmerged feature migrations were regenerated +through `pnpm db:generate` as 0340 (project tracking), 0341 (retained source removal) +and 0342 (active-source identity indexes). Their replay-safe SQL preserves installed +content and provenance. Migration numbering/safety and five-snapshot retention +pass. All 151 affected source, authorization, catalog-version and Codex cases +pass across eight suites on the merged checkout, along with recursive typecheck, +production build, maps, catalog and token gates. Inherited budget/secret review +sources retain their prior classifications; no extra provider proof is inferred. + +The final merged Storybook build succeeds and all 14 project-skills browser +journeys pass against it. This includes the inherited shell updates; no Linux +visual baseline comparison is claimed. Final publication and current-head PR +gates are recorded on the PR. + +CI caught a stale replay-test filename after regeneration. The test now opens +0340_salty_vance_astro.sql; its preservation assertions remain unchanged. The +wider macOS Node 26 database run hit eight unrelated native PostgreSQL deadlines. +The qualified Node 24 rerun passed all 51 database files and all 195 tests, with +no skips or changed deadlines. Its input records e3b107976f plus the one-line +fixture correction and the corrected fixture hash. This supplements the 151 +merged service/runtime cases and 14 rebuilt browser journeys. Final PR checks +remain the authoritative merge record. + +The last concurrent catalog merge integrates master 252d9b5077. Its background +trace-context and runtime-skill suites pass all 12 cases; recursive typecheck +and build pass. Feature controls and fixtures are unchanged from the 14-case +built-browser qualification. The verification inventory now records 59 remaining +gaps and preserves variable-dialog and production obligations for the partially +covered routine-run journey. Catalog/map checks pass without treating source +classification or test presence as additional execution proof. + +The final resource review adds generated migration 0343 and durable automatic +refresh credits: unchanged Git trees require no download, including empty commits; +512 MiB of scan/download allowance refills per hour, with conservative reservations +and refunds for small repositories. Manual Refresh remains available. Initial +imports admit 20 jobs per project and 100 per company; overflow remains durable +and is admitted as capacity frees. Repository attachment requires workspace-write +permission, and skill-import policy is checked before provider reads. Policy denials +stop automatic retries until an explicit repair/adoption. These changes passed +all 61 qualified Node 24 files and 397 tests without skips or deadline changes. +The final PUT-route and legacy-tree-learning follow-up passed all 48 affected +cases; recursive typecheck and production build pass afterward. + +The final running checkout (32c5cdc4ee plus the recorded PUT-route/tree-learning +follow-up) also passed isolated runtime doctor. Through the real board UI at +127.0.0.1:3107, a new disposable project was created without repositories, then +the approved private acceptance repository was attached through Configuration. +The saved workspace persisted after reload. Sources showed both skills imported +automatically under the new default-branch source without Refresh or adoption. +The earlier projects track main explicitly, so their existing source and versions +remain independent. The picker warned that another connection could not load; +the selected Acceptance GitHub connection successfully listed and attached the +repository. This is an additional real attachment check, not another model run. +The screenshot is retained in the inspectable evidence gallery. + +A later master merge introduced 0340_open_warlock. All unmerged feature schema +changes were regenerated together as 0341_abnormal_bucky through pnpm db:generate, +with the same preview replay safeguards and five newest snapshots. Agent setup +now pins only a GitHub connection/grant already authorized in the persisted active +run identity. Responsibility remains attributable but does not authorize searching +the person's other connections. Missing run selection and legacy agent sources +require an explicit human connection repair before unattended reads. + +Final-head 684105f683 passed all 402 qualified Node 24 cases across 62 files +and all 14 rebuilt browser journeys; CI and security also passed. Greptile then +reported a deferred-admission/detach race and stale generated Codex settings. +Both regressions failed before correction. Admission now conditionally updates +only still-pending, capacity-waiting bindings with a workspace. Generated homes +include selected config.toml content in their identity, so edits and removals +reach the launched process without carrying old private inventory. Focused +verification and fresh final-head review are recorded on the PR. + +Both final review fixes pass all 54 affected worker/Codex cases, recursive +typecheck and production build. Master c7f4b74111 changed workspace preparation +and its catalog notes; the catalog conflict preserves both sets of stable IDs +and classifications. Its affected workspace tests and final CI/review are +recorded on the PR. No new feature canvas or provider run is inferred from +this source reconciliation. + +A subsequent queue review reproduced two database failures: adoption held the +binding while waiting for the company lock, and 100 deferred rows from a full +project hid another project's work. Both regressions fail on the previous head. +Adoption now captures run identity and takes the company queue lock before +source/binding locks, reusing that validated setup while enqueueing. Admission +excludes full projects before its bounded waiting-row selection. Verification +and current-head gates are recorded on the PR. + +The final initial-import resource finding is corrected with durable company +credits, reserved before provider work. The failing-download regression observed +five reads before correction instead of the allowed four. New cases cover +worker recreation/refill, small-import refunds and completed large measured +jobs retaining their charge. These are simulated download sizes with real DB +transactions, not new oversized live downloads. Generated migration and final +qualification results are recorded on the PR. + +The initial-budget commit passes all 251 qualified Node 24 database/source/setup +cases across 54 files. The final refill review reproduced a backwards timestamp +from interleaved older/newer sweeps. Both initial-import and source-refresh +reservations now clamp time to the persisted budget timestamp. The real database +regression fails before correction and verifies no repeated refill afterward. +Focused final verification and current-head CI/review are recorded on the PR. + +Qualified Node 24 reproduced the earlier timestamp assertion failure, then passed +all 57 affected cases on the corrected source with no skips or deadline changes. +The macOS reproduction timed out in native DB setup; its broader attempt hit a +privacy-test deadline. Those failures are retained separately from the qualified +pass. Recursive typecheck and production build pass after the correction. + +Final head 0fa7fd008b passed fresh security and Greptile 5/5; tests completed +without failures before another master merge introduced a catalog conflict. +Master 7eea9d8214 moved Storybook metadata into each story and made generated +indexes ignored. The two project-skill suites now carry their existing capability +associations, production/fixture classification and durable limits inline, keeping +all explicit meta IDs and exports. Both feature recipes and master's verification +sections are retained. The deleted aggregate catalog/review ledger stays deleted. +Map/generator validation passes; rebuilt browser and final-head gates are recorded +on the PR. This structural migration is separate from the earlier live receipts. + +The final catalog-merge CI attempt lost eight jobs to the same runner shutdown; +the logs show cancellation rather than failed assertions. Greptile separately +reproduced adoption restoring a stale workspace selection. Adoption now shares +the editor's root-provenance/project/workspace lock order, re-reads the selected +repository, rechecks project access and rejects a changed selection before queue +mutations. Its real edit-interleaving regression fails before correction. Final +focused results and fresh CI/review are recorded on the PR. + +The stale-adoption correction passes all 61 affected Node 24 cases across four +suites with no skipped tests or changed deadlines. The macOS broader attempt +timed out starting two native databases; it did not establish a code failure. +Recursive typecheck and production build pass. The final rebuilt Storybook +publication 38464595162 pins 60537b0695 and contains all eight live receipts; +the adoption-only correction leaves its controls and fixtures unchanged. diff --git a/maps/agent-access-map/projects.md b/maps/agent-access-map/projects.md index bdec3cf2aa3..8803df24f07 100644 --- a/maps/agent-access-map/projects.md +++ b/maps/agent-access-map/projects.md @@ -111,3 +111,12 @@ remaining operations before qualifying the whole capability. Gaps: Description/goal/target selection exist; host workspace command mutation is denied to agents and budget policy is board-managed. + +Repository attachment also writes a durable skill import binding in its transaction. +Agent-created bindings derive their refresh person from authenticated responsible-user +context. Project selection does not grant runtime credentials or override private +project skill access. Verify automatic import and denial with the actual agent identity. + +Workspace creation, update and deletion require project:write_workspace through +the shared authorization evaluator in addition to project read access. Repository +attachment does not bypass skill-import policy for its background discovery. diff --git a/maps/agent-access-map/routines.md b/maps/agent-access-map/routines.md index 19dd9076985..9abfe59048e 100644 --- a/maps/agent-access-map/routines.md +++ b/maps/agent-access-map/routines.md @@ -146,3 +146,9 @@ remaining operations before qualifying the whole capability. Gaps: Pause/archive mutations have a generic native REST path under routine ownership rules. Plugin-managed routine rules remain separate. + +Execution resolves referenced project skills against agent/responsible-user access, +then saves immutable version selections in the heartbeat execution context. New runs +resolve latest installed content; resumed runs retain selections and recheck access. +An unavailable explicit reference produces an actionable configuration error. +Live agent execution across an upstream update remains required acceptance evidence. diff --git a/maps/agent-access-map/skills.md b/maps/agent-access-map/skills.md index 2231414834f..5f2b862ecf6 100644 --- a/maps/agent-access-map/skills.md +++ b/maps/agent-access-map/skills.md @@ -142,3 +142,37 @@ remaining operations before qualifying the whole capability. Gaps: No dedicated native orchestration binding; generic REST calls retain skills.test policy, task-assignment authorization, responsible-user intersection and admitted skill-test JWT scope. + +Project-source API additions: GET `/api/companies/{companyId}/skill-sources/project-repositories`, +POST `/api/companies/{companyId}/skill-sources/adopt-project` with `workspaceId`, and +PATCH `/api/companies/{companyId}/skill-sources/{sourceId}/settings` with expected revision +and update/connection preferences. Native API discovery uses the OpenAPI registration. + +Project-derived read/use permission follows current project authorization, with access +through either originating project. File/version reads, source metadata, mutations, +editable copies, tests and runtime delivery retain that boundary. Unattended refresh +uses the revalidated setup identity and connection, not the requesting agent's token. +These additions need actual permitted/denied agent-run acceptance; service tests alone +are not live credential proof. + +Direct GitHub source imports share the project setup-identity boundary. API-key +agents may import permitted public content, but their responsible-user field +does not authorize unattended credential use. They need human connection repair +for automatic refresh. Authenticated agent runs derive the setup person from +the persisted active run. Legacy agent activity without a matching run cannot +recover a refresh person. Route and database tests cover these distinctions; +this addition makes no new live-provider acceptance claim. + +Initial project imports check skills.import before provider reads; denied jobs +do not retry automatically. Deferred jobs remain durable behind per-project and +company admission limits. Scheduled refresh compares immutable tree identity and +uses a persistent work allowance; manual refresh still requires its existing +policy/access checks. These limits grant no content or execution access. + +Agent repository setup preserves the active run's selected GitHub connection/grant +IDs. Missing selections require human repair; the worker never searches the +responsible person's other grants for an agent-created source. + +Initial automatic imports reserve company work credits before provider reads. +Failures retain the reservation; completed jobs cannot replenish the queue's +work allowance. Jobs remain durable and resume as company credits refill. diff --git a/maps/feature-map/connection-setup.md b/maps/feature-map/connection-setup.md index bc4257662a8..f818fded54c 100644 --- a/maps/feature-map/connection-setup.md +++ b/maps/feature-map/connection-setup.md @@ -108,6 +108,8 @@ Test no-auth and authenticated endpoints separately; never infer one from the ot Automated: [interaction card](../../ui/src/components/IssueThreadInteractionCard.test.tsx) checks addressed-user controls. AppsConnect tests cover page/dialog parity, task-bound OAuth, popup handoff, and verifying a saved connection before closing. +Withdrawn requests render a terminal acknowledgement with no setup action; the +interaction-body test and narrow Storybook journey cover this recovery state. The connection-intents browser suite above exercises fixture-backed continuation. Manual: ask an agent to use an unavailable app, open its request as the named diff --git a/maps/feature-map/projects.md b/maps/feature-map/projects.md index 6e4d042cc8d..148731b9613 100644 --- a/maps/feature-map/projects.md +++ b/maps/feature-map/projects.md @@ -56,6 +56,14 @@ Manual: Attach a small repository, save, and create a project task. Inspect the - See [workspaces](./workspaces.md), [goals](./goals.md), and [budgets](./budgets-costs.md) for those features. - Do not assume a deleted or archived project should erase its historical task evidence. +### Automatic repository skills + +Creating or attaching a GitHub repository enqueues skill import without another setup +step. Check Skills → Sources and the installed library afterward. Ordinary edits to +older projects do not adopt their repositories; explicit adoption belongs in Sources. +Removing a repository retains installed skills and originating project access but stops +its automatic tracking. Changing the ref does not retarget existing skill references. + ### Documentation scope: Projects and execution workspaces Provider-specific provisioning, multi-repository/submodule edge cases, Git reconciliation variants, and all service exposure combinations require deeper recipes. diff --git a/maps/feature-map/routines.md b/maps/feature-map/routines.md index 77a468d19fd..f64b5b19724 100644 --- a/maps/feature-map/routines.md +++ b/maps/feature-map/routines.md @@ -60,6 +60,14 @@ Manual: Set a near-term disposable schedule or authorized test webhook, verify o and verify the same generation before stopping. Local execution and catch-up remain unchanged. Unsupported triggers and other pending work keep it awake. +### Latest skill references + +Routine instructions retain stable skill mentions. After a project source syncs an +upstream change, the next execution uses the latest installed version without waiting +on GitHub. Record the resolved version before/after and verify a resumed execution +keeps its original version. Private skill references require current project access +for both the agent and its responsible person. + ### Storybook verification #### `run-now` diff --git a/maps/feature-map/skills.md b/maps/feature-map/skills.md index 276bbcbbcbf..91a1d5d7c3a 100644 --- a/maps/feature-map/skills.md +++ b/maps/feature-map/skills.md @@ -66,6 +66,21 @@ Manual: Enable the skill for one test agent and leave another without access. Ru - Read-only GitHub/bundled skills may require a writable copy rather than an in-place edit. - Studio interaction coverage is in [questions and approvals](./questions-and-approvals.md); a passing mock is not a live harness test. +### Project repository source behavior + +New GitHub project repositories import skills automatically. Skills → Sources lists +accessible legacy repositories with Import skills, shows source/project provenance, +and provides Refresh and Automatic updates in its source menu. Verify future additions, +retained exclusions, error recovery and reload persistence. Private-only sources must +be absent for unrelated people/agents; access through either originating project is sufficient. + +Storybook/browser coverage uses scoped fixtures. Live GitHub-to-routine execution +must be verified separately; inspect resolved version IDs across an upstream update. + +### Connection repair during an update + +Open Choose connection while a source is being refreshed. Select an authorized account and save. If the source changes, the dialog must retain that selection, load the current revision, and allow a second save without reopening. The [project repository browser suite](../../tests/storybook-visual/project-repository-skills.spec.ts) covers this recovery with simulated upstream data. + ### Storybook verification #### `testing` diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index d0c3ef76675..e81f83f23dd 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -1,5 +1,6 @@ import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary"; import { createUsageCheckpointLog } from "@paperclipai/adapter-utils/usage-checkpoint"; +import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -9,6 +10,7 @@ import { copyBackCodexAuth } from "./codex-auth-copyback.js"; import { ensureCodexAuthCacheEntryDir, isCodexAuthCacheEnabled, + readSubscriptionAccountId, resolveCodexAuthCacheEntryPath, selectVendCredential, } from "./codex-auth-cache.js"; @@ -642,27 +644,50 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? path.resolve(envConfig.CODEX_HOME.trim()) : null; - const connectorSourceHome = configuredCodexHome; - const connectorSkillDigest = typeof config.paperclipConnectorSkillDigest === "string" - && /^[a-f0-9]{64}$/.test(config.paperclipConnectorSkillDigest) ? config.paperclipConnectorSkillDigest : null; - if (connectorSkillDigest) { - // Never mount assignment-specific skills into the shared company/user home. - // A different skill revision gets a new home, so revoked/changed resources - // cannot survive as stale symlinks or bleed into another agent's session. - configuredCodexHome = path.join(resolveManagedCodexHomeDir(process.env, agent.companyId), - "connector-runtimes", agent.id, connectorSkillDigest); - } - const codexSkillEntries = (await readPaperclipRuntimeSkillEntries(config, __moduleDir)) - // A missing-source entry would become a dangling skill symlink; skip it. - .filter((entry) => !isPaperclipSkillSourceMissing(entry)); - const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries); - if (!executionTargetIsRemote) { - await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); - } const configuredOpenAiApiKey = typeof envConfig.OPENAI_API_KEY === "string" && envConfig.OPENAI_API_KEY.trim().length > 0 ? envConfig.OPENAI_API_KEY.trim() : null; + const skillSourceHome = configuredCodexHome; + const connectorSkillDigest = typeof config.paperclipConnectorSkillDigest === "string" + && /^[a-f0-9]{64}$/.test(config.paperclipConnectorSkillDigest) ? config.paperclipConnectorSkillDigest : null; + const codexSkillEntries = (await readPaperclipRuntimeSkillEntries(config, __moduleDir)) + .filter((entry) => !isPaperclipSkillSourceMissing(entry)); + const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries); + const desiredSet = new Set(desiredSkillNames); + // An explicit inventory is the server's authorized execution selection. Include + // empty inventories: revocation must not fall back to a previously populated home. + let runtimeSkillDigest = Array.isArray(config.paperclipRuntimeSkills) + ? createHash("sha256").update(JSON.stringify({ connectorSkillDigest, + skills: codexSkillEntries.filter(entry => desiredSet.has(entry.key)) + .map(entry => [entry.key, entry.source]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))), + })).digest("hex") + : connectorSkillDigest; + const runtimeCredentialHome = runtimeSkillDigest + ? skillSourceHome ?? await prepareManagedCodexHome(process.env, onLog, agent.companyId, { apiKey: configuredOpenAiApiKey }) + : null; + if (runtimeSkillDigest) { + const credentialHome = runtimeCredentialHome!; + const credentialBytes = await fs.readFile(path.join(credentialHome, "auth.json")).catch(() => null); + const selectedConfig = await fs.readFile(path.join(credentialHome, "config.toml")).catch(() => null); + // Keep selected-home provider/MCP edits (and removals) in the launched home. + const selectedConfigDigest = selectedConfig ? createHash("sha256").update(selectedConfig).digest("hex") : null; + // Copy-back may retain a refreshed regular credential in a generated home. + // Account switches must select a different home, while token refreshes for + // the same subscription identity preserve its cache and session state. + const credentialIdentity = credentialBytes + ? readSubscriptionAccountId(credentialBytes) ?? createHash("sha256").update(credentialBytes).digest("hex") : null; + runtimeSkillDigest = createHash("sha256").update(JSON.stringify({ + runtimeSkillDigest, credentialHome, credentialIdentity, selectedConfigDigest, apiKey: envConfig.OPENAI_API_KEY ?? null, + })).digest("hex"); + // Different authorized versions/assignments get separate homes, including + // session state. Previously permitted private links cannot survive revocation. + configuredCodexHome = path.join(resolveManagedCodexHomeDir(process.env, agent.companyId), + connectorSkillDigest ? "connector-runtimes" : "skill-runtimes", agent.id, runtimeSkillDigest); + } + if (!executionTargetIsRemote) { + await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); + } // A configured CODEX_HOME that lives under the Paperclip-managed company tree // (the per-agent home set by the server isolation guard) still needs auth // seeded — it ships with no credentials and OPENAI_API_KEY="" by default. @@ -697,14 +722,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise typeof entry[1] === "string", @@ -745,7 +771,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise statement-breakpoint +ALTER TABLE "company_skill_sources" DROP CONSTRAINT IF EXISTS "company_skill_sources_connection_id_tool_connections_id_fk"; +--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_updates" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "import_new_skills" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "access_scope" text DEFAULT 'company' NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_user_id" text;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_agent_id" uuid;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_local_trusted" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_grant_id" uuid;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "next_sync_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "failure_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "access_source_id" uuid;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_company_id_companies_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN + ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_project_id_projects_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN + ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_workspace_id_project_workspaces_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN + ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_workspace_id_project_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."project_workspaces"("id") ON DELETE set null ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_source_id_company_skill_sources_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN + ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_source_id_company_skill_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."company_skill_sources"("id") ON DELETE set null ON UPDATE no action; + END IF; +END $$;--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "project_skill_sources_workspace_repo_ref_idx" ON "project_skill_sources" USING btree ("workspace_id","repository_url","tracking_ref");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "project_skill_sources_source_project_idx" ON "project_skill_sources" USING btree ("company_id","source_id","project_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "project_skill_sources_sync_idx" ON "project_skill_sources" USING btree ("active","status","next_sync_at"); +--> statement-breakpoint +-- Retain setup identity if a connection or agent is deleted, including preview schemas. +ALTER TABLE "company_skill_sources" DROP CONSTRAINT IF EXISTS "company_skill_sources_refresh_agent_id_agents_id_fk"; +--> statement-breakpoint +ALTER TABLE "project_skill_sources" DROP CONSTRAINT IF EXISTS "project_skill_sources_refresh_agent_id_agents_id_fk"; +--> statement-breakpoint +ALTER TABLE "project_skill_sources" DROP CONSTRAINT IF EXISTS "project_skill_sources_connection_id_tool_connections_id_fk"; + +--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "removed_at" timestamp with time zone; +--> statement-breakpoint +DROP INDEX IF EXISTS "company_skill_sources_repository_ref_idx";--> statement-breakpoint +DROP INDEX IF EXISTS "company_skill_sources_identity_ref_idx";--> statement-breakpoint +CREATE UNIQUE INDEX "company_skill_sources_repository_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_url","tracking_ref") WHERE "company_skill_sources"."removed_at" is null;--> statement-breakpoint +CREATE UNIQUE INDEX "company_skill_sources_identity_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_id","tracking_ref") WHERE "company_skill_sources"."removed_at" is null; +--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "last_scan_tree_sha" text;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_budget_bytes" integer DEFAULT 536870912 NOT NULL;--> statement-breakpoint +ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_budget_at" timestamp with time zone DEFAULT now() NOT NULL; \ No newline at end of file diff --git a/packages/db/src/migrations/0342_dry_shen.sql b/packages/db/src/migrations/0342_dry_shen.sql new file mode 100644 index 00000000000..b5ea93a9b4a --- /dev/null +++ b/packages/db/src/migrations/0342_dry_shen.sql @@ -0,0 +1,7 @@ +CREATE TABLE "company_skill_import_budgets" ( + "company_id" uuid PRIMARY KEY NOT NULL, + "available_bytes" integer DEFAULT 536870912 NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "company_skill_import_budgets" ADD CONSTRAINT "company_skill_import_budgets_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0337_snapshot.json b/packages/db/src/migrations/meta/0341_snapshot.json similarity index 98% rename from packages/db/src/migrations/meta/0337_snapshot.json rename to packages/db/src/migrations/meta/0341_snapshot.json index ac4a041b294..1c38823f95a 100644 --- a/packages/db/src/migrations/meta/0337_snapshot.json +++ b/packages/db/src/migrations/meta/0341_snapshot.json @@ -1,6 +1,6 @@ { - "id": "65f48b10-7f94-449e-b2ce-a68f220910f3", - "prevId": "a9f48522-347d-4290-83dd-07299afb8d03", + "id": "927d7532-059b-4a4f-846a-5e3445a81891", + "prevId": "b32fa655-79c7-4793-a021-22258b6c369c", "version": "7", "dialect": "postgresql", "tables": { @@ -16324,6 +16324,18 @@ "primaryKey": false, "notNull": false }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_plugin_id": { + "name": "owner_plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "user_secret_definition_id": { "name": "user_secret_definition_id", "type": "uuid", @@ -16552,6 +16564,40 @@ "method": "btree", "with": {} }, + "company_secrets_instance_owner_key_uq": { + "name": "company_secrets_instance_owner_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'instance' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, "company_secrets_provider_config_idx": { "name": "company_secrets_provider_config_idx", "columns": [ @@ -16654,6 +16700,19 @@ "onDelete": "no action", "onUpdate": "no action" }, + "company_secrets_owner_plugin_id_plugins_id_fk": { + "name": "company_secrets_owner_plugin_id_plugins_id_fk", + "tableFrom": "company_secrets", + "tableTo": "plugins", + "columnsFrom": [ + "owner_plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": { "name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk", "tableFrom": "company_secrets", @@ -16692,6 +16751,21 @@ ], "onDelete": "set null", "onUpdate": "no action" + }, + "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk": { + "name": "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "company_id", + "owner_agent_id" + ], + "columnsTo": [ + "company_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -16700,7 +16774,7 @@ "checkConstraints": { "company_secrets_scope_shape_check": { "name": "company_secrets_scope_shape_check", - "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )" + "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'instance'\n and \"company_secrets\".\"owner_user_id\" is null and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is not null and \"company_secrets\".\"owner_plugin_id\" is not null\n )" } }, "isRLSEnabled": false @@ -17017,6 +17091,72 @@ "notNull": true, "default": true }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "automatic_updates": { + "name": "automatic_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "import_new_skills": { + "name": "import_new_skills", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "access_scope": { + "name": "access_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "refresh_user_id": { + "name": "refresh_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_agent_id": { + "name": "refresh_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_local_trusted": { + "name": "refresh_local_trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "refresh_grant_id": { + "name": "refresh_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, "revision": { "name": "revision", "type": "integer", @@ -17042,6 +17182,26 @@ "primaryKey": false, "notNull": false }, + "last_scan_tree_sha": { + "name": "last_scan_tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automatic_budget_bytes": { + "name": "automatic_budget_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 536870912 + }, + "automatic_budget_at": { + "name": "automatic_budget_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, "last_error": { "name": "last_error", "type": "text", @@ -17092,6 +17252,7 @@ } ], "isUnique": true, + "where": "\"company_skill_sources\".\"removed_at\" is null", "concurrently": false, "method": "btree", "with": {} @@ -17119,6 +17280,7 @@ } ], "isUnique": true, + "where": "\"company_skill_sources\".\"removed_at\" is null", "concurrently": false, "method": "btree", "with": {} @@ -17137,13 +17299,275 @@ ], "onDelete": "cascade", "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_skill_sources": { + "name": "project_skill_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" }, - "company_skill_sources_connection_id_tool_connections_id_fk": { - "name": "company_skill_sources_connection_id_tool_connections_id_fk", - "tableFrom": "company_skill_sources", - "tableTo": "tool_connections", + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository_url": { + "name": "repository_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_ref": { + "name": "tracking_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "refresh_user_id": { + "name": "refresh_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_agent_id": { + "name": "refresh_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_local_trusted": { + "name": "refresh_local_trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_grant_id": { + "name": "refresh_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_skill_sources_workspace_repo_ref_idx": { + "name": "project_skill_sources_workspace_repo_ref_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tracking_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_skill_sources_source_project_idx": { + "name": "project_skill_sources_source_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_skill_sources_sync_idx": { + "name": "project_skill_sources_sync_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_skill_sources_company_id_companies_id_fk": { + "name": "project_skill_sources_company_id_companies_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "companies", "columnsFrom": [ - "connection_id" + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_skill_sources_project_id_projects_id_fk": { + "name": "project_skill_sources_project_id_projects_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_skill_sources_workspace_id_project_workspaces_id_fk": { + "name": "project_skill_sources_workspace_id_project_workspaces_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "project_workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "project_skill_sources_source_id_company_skill_sources_id_fk": { + "name": "project_skill_sources_source_id_company_skill_sources_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "company_skill_sources", + "columnsFrom": [ + "source_id" ], "columnsTo": [ "id" @@ -18603,6 +19027,12 @@ "notNull": true, "default": "'company'" }, + "access_source_id": { + "name": "access_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "public_share_token": { "name": "public_share_token", "type": "text", @@ -26839,6 +27269,136 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.execution_workspace_repository_access": { + "name": "execution_workspace_repository_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_key": { + "name": "root_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_cwd": { + "name": "canonical_cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_repository_access_scope_idx": { + "name": "execution_workspace_repository_access_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_repository_access_company_id_companies_id_fk": { + "name": "execution_workspace_repository_access_company_id_companies_id_fk", + "tableFrom": "execution_workspace_repository_access", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.execution_workspace_repository_roots": { "name": "execution_workspace_repository_roots", "schema": "", @@ -45996,7 +46556,7 @@ }, "resource_lifecycle_events_action_check": { "name": "resource_lifecycle_events_action_check", - "value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))" + "value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive', 'delete')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))" } }, "isRLSEnabled": false diff --git a/packages/db/src/migrations/meta/0336_snapshot.json b/packages/db/src/migrations/meta/0342_snapshot.json similarity index 98% rename from packages/db/src/migrations/meta/0336_snapshot.json rename to packages/db/src/migrations/meta/0342_snapshot.json index 19604c5e2f5..d29b15d8650 100644 --- a/packages/db/src/migrations/meta/0336_snapshot.json +++ b/packages/db/src/migrations/meta/0342_snapshot.json @@ -1,6 +1,6 @@ { - "id": "a9f48522-347d-4290-83dd-07299afb8d03", - "prevId": "18c156f2-8344-4282-98b7-f702f0035c81", + "id": "e9a4bbc2-b22c-48a1-a74c-f108cd7b20ec", + "prevId": "927d7532-059b-4a4f-846a-5e3445a81891", "version": "7", "dialect": "postgresql", "tables": { @@ -16324,6 +16324,18 @@ "primaryKey": false, "notNull": false }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_plugin_id": { + "name": "owner_plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "user_secret_definition_id": { "name": "user_secret_definition_id", "type": "uuid", @@ -16552,6 +16564,40 @@ "method": "btree", "with": {} }, + "company_secrets_instance_owner_key_uq": { + "name": "company_secrets_instance_owner_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'instance' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, "company_secrets_provider_config_idx": { "name": "company_secrets_provider_config_idx", "columns": [ @@ -16654,6 +16700,19 @@ "onDelete": "no action", "onUpdate": "no action" }, + "company_secrets_owner_plugin_id_plugins_id_fk": { + "name": "company_secrets_owner_plugin_id_plugins_id_fk", + "tableFrom": "company_secrets", + "tableTo": "plugins", + "columnsFrom": [ + "owner_plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": { "name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk", "tableFrom": "company_secrets", @@ -16692,6 +16751,21 @@ ], "onDelete": "set null", "onUpdate": "no action" + }, + "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk": { + "name": "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "company_id", + "owner_agent_id" + ], + "columnsTo": [ + "company_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -16700,7 +16774,7 @@ "checkConstraints": { "company_secrets_scope_shape_check": { "name": "company_secrets_scope_shape_check", - "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )" + "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'instance'\n and \"company_secrets\".\"owner_user_id\" is null and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is not null and \"company_secrets\".\"owner_plugin_id\" is not null\n )" } }, "isRLSEnabled": false @@ -16778,6 +16852,53 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.company_skill_import_budgets": { + "name": "company_skill_import_budgets", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "available_bytes": { + "name": "available_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 536870912 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "company_skill_import_budgets_company_id_companies_id_fk": { + "name": "company_skill_import_budgets_company_id_companies_id_fk", + "tableFrom": "company_skill_import_budgets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.company_skill_source_entries": { "name": "company_skill_source_entries", "schema": "", @@ -17017,6 +17138,72 @@ "notNull": true, "default": true }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "automatic_updates": { + "name": "automatic_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "import_new_skills": { + "name": "import_new_skills", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "access_scope": { + "name": "access_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "refresh_user_id": { + "name": "refresh_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_agent_id": { + "name": "refresh_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_local_trusted": { + "name": "refresh_local_trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "refresh_grant_id": { + "name": "refresh_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, "revision": { "name": "revision", "type": "integer", @@ -17042,6 +17229,26 @@ "primaryKey": false, "notNull": false }, + "last_scan_tree_sha": { + "name": "last_scan_tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automatic_budget_bytes": { + "name": "automatic_budget_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 536870912 + }, + "automatic_budget_at": { + "name": "automatic_budget_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, "last_error": { "name": "last_error", "type": "text", @@ -17092,6 +17299,7 @@ } ], "isUnique": true, + "where": "\"company_skill_sources\".\"removed_at\" is null", "concurrently": false, "method": "btree", "with": {} @@ -17119,6 +17327,7 @@ } ], "isUnique": true, + "where": "\"company_skill_sources\".\"removed_at\" is null", "concurrently": false, "method": "btree", "with": {} @@ -17137,13 +17346,275 @@ ], "onDelete": "cascade", "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_skill_sources": { + "name": "project_skill_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" }, - "company_skill_sources_connection_id_tool_connections_id_fk": { - "name": "company_skill_sources_connection_id_tool_connections_id_fk", - "tableFrom": "company_skill_sources", - "tableTo": "tool_connections", + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository_url": { + "name": "repository_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_ref": { + "name": "tracking_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "refresh_user_id": { + "name": "refresh_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_agent_id": { + "name": "refresh_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_local_trusted": { + "name": "refresh_local_trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_grant_id": { + "name": "refresh_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_skill_sources_workspace_repo_ref_idx": { + "name": "project_skill_sources_workspace_repo_ref_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tracking_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_skill_sources_source_project_idx": { + "name": "project_skill_sources_source_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_skill_sources_sync_idx": { + "name": "project_skill_sources_sync_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_skill_sources_company_id_companies_id_fk": { + "name": "project_skill_sources_company_id_companies_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "companies", "columnsFrom": [ - "connection_id" + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_skill_sources_project_id_projects_id_fk": { + "name": "project_skill_sources_project_id_projects_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_skill_sources_workspace_id_project_workspaces_id_fk": { + "name": "project_skill_sources_workspace_id_project_workspaces_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "project_workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "project_skill_sources_source_id_company_skill_sources_id_fk": { + "name": "project_skill_sources_source_id_company_skill_sources_id_fk", + "tableFrom": "project_skill_sources", + "tableTo": "company_skill_sources", + "columnsFrom": [ + "source_id" ], "columnsTo": [ "id" @@ -18603,6 +19074,12 @@ "notNull": true, "default": "'company'" }, + "access_source_id": { + "name": "access_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "public_share_token": { "name": "public_share_token", "type": "text", @@ -26839,6 +27316,230 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.execution_workspace_repository_access": { + "name": "execution_workspace_repository_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_key": { + "name": "root_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_cwd": { + "name": "canonical_cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_repository_access_scope_idx": { + "name": "execution_workspace_repository_access_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_repository_access_company_id_companies_id_fk": { + "name": "execution_workspace_repository_access_company_id_companies_id_fk", + "tableFrom": "execution_workspace_repository_access", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspace_repository_roots": { + "name": "execution_workspace_repository_roots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "canonical_cwd": { + "name": "canonical_cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "root_key": { + "name": "root_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_repository_roots_company_root_idx": { + "name": "execution_workspace_repository_roots_company_root_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_repository_roots_company_id_companies_id_fk": { + "name": "execution_workspace_repository_roots_company_id_companies_id_fk", + "tableFrom": "execution_workspace_repository_roots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.execution_workspace_runtime_leases": { "name": "execution_workspace_runtime_leases", "schema": "", @@ -45902,7 +46603,7 @@ }, "resource_lifecycle_events_action_check": { "name": "resource_lifecycle_events_action_check", - "value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))" + "value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive', 'delete')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))" } }, "isRLSEnabled": false diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 248bad8e5b5..2bd7d8fcc9b 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -2374,6 +2374,20 @@ "when": 1791911743153, "tag": "0340_open_warlock", "breakpoints": true + }, + { + "idx": 341, + "version": "7", + "when": 1791919024567, + "tag": "0341_abnormal_bucky", + "breakpoints": true + }, + { + "idx": 342, + "version": "7", + "when": 1791921858146, + "tag": "0342_dry_shen", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/project-skill-sources-migration.test.ts b/packages/db/src/project-skill-sources-migration.test.ts new file mode 100644 index 00000000000..dbb2e240d44 --- /dev/null +++ b/packages/db/src/project-skill-sources-migration.test.ts @@ -0,0 +1,54 @@ +import { randomUUID } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import postgres from "postgres"; +import { describe, expect, it } from "vitest"; +import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js"; + +const support = await getEmbeddedPostgresTestSupport(); +const describePostgres = support.supported ? describe : describe.skip; + +describePostgres("project skill source migration replay", () => { + it("keeps imported content, private provenance and scheduling when renumbered SQL runs again", async () => { + const database = await startEmbeddedPostgresTestDatabase("paperclip-project-skill-migration-"); + const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} }); + try { + const companyId = randomUUID(), projectId = randomUUID(), workspaceId = randomUUID(); + const sourceId = randomUUID(), skillId = randomUUID(), bindingId = randomUUID(), agentId = randomUUID(); + const connectionId = randomUUID(); + await sql`INSERT INTO companies (id, name, issue_prefix) VALUES (${companyId}, 'Migration company', 'PSM')`; + await sql`INSERT INTO projects (id, company_id, name, visibility) VALUES (${projectId}, ${companyId}, 'Private project', 'private')`; + await sql`INSERT INTO project_workspaces (id, company_id, project_id, name, repo_url) VALUES (${workspaceId}, ${companyId}, ${projectId}, 'Repository', 'https://github.com/acme/skills')`; + await sql`INSERT INTO agents (id, company_id, name) VALUES (${agentId}, ${companyId}, 'Setup agent')`; + await sql`INSERT INTO company_skill_sources (id, company_id, repository_url, full_name, tracking_ref, access_scope, automatic_updates, import_new_skills, refresh_user_id, refresh_agent_id, connection_id, excluded_folders, revision, failure_count, next_sync_at) + VALUES (${sourceId}, ${companyId}, 'https://github.com/acme/skills', 'acme/skills', 'HEAD', 'projects', false, true, 'setup-person', ${agentId}, ${connectionId}, '["excluded"]', 7, 3, '2030-01-01')`; + await sql`INSERT INTO company_skills (id, company_id, key, slug, name, markdown, access_source_id) VALUES (${skillId}, ${companyId}, 'repo/review', 'review', 'Review', 'Keep installed content', ${sourceId})`; + await sql`INSERT INTO project_skill_sources (id, company_id, project_id, workspace_id, source_id, repository_url, tracking_ref, active, status, refresh_user_id, refresh_agent_id, connection_id) + VALUES (${bindingId}, ${companyId}, ${projectId}, ${workspaceId}, ${sourceId}, 'https://github.com/acme/skills', 'HEAD', false, 'ready', 'setup-person', ${agentId}, ${connectionId})`; + // Preview builds used an agent FK that would erase the authorizing identity. + await sql.unsafe('ALTER TABLE project_skill_sources ADD CONSTRAINT project_skill_sources_refresh_agent_id_agents_id_fk FOREIGN KEY (refresh_agent_id) REFERENCES agents(id) ON DELETE SET NULL'); + const source = () => sql`SELECT * FROM company_skill_sources WHERE id = ${sourceId}`; + const binding = () => sql`SELECT * FROM project_skill_sources WHERE id = ${bindingId}`; + const skill = () => sql`SELECT * FROM company_skills WHERE id = ${skillId}`; + const relations = () => sql`SELECT oid::text, relname FROM pg_class WHERE relname IN ('company_skill_sources', 'project_skill_sources', 'company_skills') ORDER BY relname`; + const before = { source: await source(), binding: await binding(), skill: await skill(), relations: await relations() }; + const migration = await readFile(new URL("./migrations/0341_abnormal_bucky.sql", import.meta.url), "utf8"); + for (let attempt = 0; attempt < 2; attempt++) { + for (const statement of migration.split("--> statement-breakpoint")) { + if (statement.trim()) await sql.unsafe(statement); + } + expect(await source()).toEqual(before.source); + expect(await binding()).toEqual(before.binding); + expect(await skill()).toEqual(before.skill); + expect(await relations()).toEqual(before.relations); + } + await sql`DELETE FROM agents WHERE id = ${agentId}`; + expect((await source())[0]?.refresh_agent_id).toBe(agentId); + expect((await binding())[0]?.refresh_agent_id).toBe(agentId); + expect((await binding())[0]?.connection_id).toBe(connectionId); + expect((await skill())[0]?.access_source_id).toBe(sourceId); + } finally { + await sql.end(); + await database.cleanup(); + } + }, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS); +}); diff --git a/packages/db/src/schema/company_skill_sources.ts b/packages/db/src/schema/company_skill_sources.ts index 58f82e2a55d..1dce4041ff7 100644 --- a/packages/db/src/schema/company_skill_sources.ts +++ b/packages/db/src/schema/company_skill_sources.ts @@ -1,8 +1,10 @@ +import { sql } from 'drizzle-orm'; import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core'; import type { SkillPackageInspection, SkillRepositoryPackage } from '@paperclipai/shared'; import { companies } from './companies.js'; import { companySkills } from './company_skills.js'; -import { toolConnections } from './tool_access.js'; +import { projects } from './projects.js'; +import { projectWorkspaces } from './project_workspaces.js'; export const companySkillSources = pgTable('company_skill_sources', { id: uuid('id').primaryKey().defaultRandom(), @@ -11,22 +13,65 @@ export const companySkillSources = pgTable('company_skill_sources', { repositoryUrl: text('repository_url').notNull(), fullName: text('full_name').notNull(), trackingRef: text('tracking_ref').notNull(), - connectionId: uuid('connection_id').references(() => toolConnections.id, { onDelete: 'set null' }), + // Retain the selected identity after deletion so refresh requires repair. + connectionId: uuid('connection_id'), packageMode: text('package_mode').$type<'skills' | 'repository'>().notNull().default('skills'), repositoryPackage: jsonb('repository_package').$type(), excludedFolders: jsonb('excluded_folders').$type().notNull().default([]), enabled: boolean('enabled').notNull().default(true), + // Removed project sources retain access provenance for editable copies and forks. + removedAt: timestamp('removed_at', { withTimezone: true }), + automaticUpdates: boolean('automatic_updates').notNull().default(true), + importNewSkills: boolean('import_new_skills').notNull().default(false), + accessScope: text('access_scope').$type<'company' | 'projects'>().notNull().default('company'), + refreshUserId: text('refresh_user_id'), + refreshAgentId: uuid('refresh_agent_id'), + refreshLocalTrusted: boolean('refresh_local_trusted').notNull().default(false), + refreshGrantId: uuid('refresh_grant_id'), + nextSyncAt: timestamp('next_sync_at', { withTimezone: true }).notNull().defaultNow(), + failureCount: integer('failure_count').notNull().default(0), revision: integer('revision').notNull().default(0), lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }), lastSuccessAt: timestamp('last_success_at', { withTimezone: true }), lastScanCommit: text('last_scan_commit'), + lastScanTreeSha: text('last_scan_tree_sha'), + automaticBudgetBytes: integer('automatic_budget_bytes').notNull().default(512 * 1024 * 1024), + automaticBudgetAt: timestamp('automatic_budget_at', { withTimezone: true }).notNull().defaultNow(), lastError: text('last_error'), leaseToken: uuid('lease_token'), leaseExpiresAt: timestamp('lease_expires_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, table => ({ - repositoryRef: uniqueIndex('company_skill_sources_repository_ref_idx').on(table.companyId, table.repositoryUrl, table.trackingRef), - repositoryIdentity: uniqueIndex('company_skill_sources_identity_ref_idx').on(table.companyId, table.repositoryId, table.trackingRef), + repositoryRef: uniqueIndex('company_skill_sources_repository_ref_idx').on(table.companyId, table.repositoryUrl, table.trackingRef).where(sql`${table.removedAt} is null`), + repositoryIdentity: uniqueIndex('company_skill_sources_identity_ref_idx').on(table.companyId, table.repositoryId, table.trackingRef).where(sql`${table.removedAt} is null`), +})); + +/** Durable attachment jobs and retained project access provenance. */ +export const projectSkillSources = pgTable('project_skill_sources', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id').notNull().references(() => companies.id, { onDelete: 'cascade' }), + projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }), + workspaceId: uuid('workspace_id').references(() => projectWorkspaces.id, { onDelete: 'set null' }), + sourceId: uuid('source_id').references(() => companySkillSources.id, { onDelete: 'set null' }), + repositoryUrl: text('repository_url').notNull(), + trackingRef: text('tracking_ref').notNull(), + active: boolean('active').notNull().default(true), + status: text('status').$type<'pending' | 'ready' | 'error'>().notNull().default('pending'), + refreshUserId: text('refresh_user_id'), + refreshAgentId: uuid('refresh_agent_id'), + refreshLocalTrusted: boolean('refresh_local_trusted').notNull().default(false), + connectionId: uuid('connection_id'), + refreshGrantId: uuid('refresh_grant_id'), + nextSyncAt: timestamp('next_sync_at', { withTimezone: true }).notNull().defaultNow(), + failureCount: integer('failure_count').notNull().default(0), + lastError: text('last_error'), + leaseToken: uuid('lease_token'), + leaseExpiresAt: timestamp('lease_expires_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}, table => ({ + workspaceRepository: uniqueIndex('project_skill_sources_workspace_repo_ref_idx').on(table.workspaceId, table.repositoryUrl, table.trackingRef), + sourceProject: index('project_skill_sources_source_project_idx').on(table.companyId, table.sourceId, table.projectId), + pending: index('project_skill_sources_sync_idx').on(table.active, table.status, table.nextSyncAt), })); export const companySkillSourceEntries = pgTable('company_skill_source_entries', { id: uuid('id').primaryKey().defaultRandom(), @@ -44,3 +89,10 @@ export const companySkillSourceEntries = pgTable('company_skill_source_entries', sourcePath: uniqueIndex('company_skill_source_entries_source_path_idx').on(table.sourceId, table.path), companySkill: index('company_skill_source_entries_company_skill_idx').on(table.companyId, table.skillId), })); + +/** Sustained company credits for automatic initial imports, including failures. */ +export const companySkillImportBudgets = pgTable('company_skill_import_budgets', { + companyId: uuid('company_id').primaryKey().references(() => companies.id, { onDelete: 'cascade' }), + availableBytes: integer('available_bytes').notNull().default(512 * 1024 * 1024), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); diff --git a/packages/db/src/schema/company_skills.ts b/packages/db/src/schema/company_skills.ts index 15bebc05629..d6ce60b2aeb 100644 --- a/packages/db/src/schema/company_skills.ts +++ b/packages/db/src/schema/company_skills.ts @@ -40,6 +40,8 @@ export const companySkills = pgTable( homepageUrl: text("homepage_url"), categories: text("categories").array().notNull().default([]), sharingScope: text("sharing_scope").$type().notNull().default("company"), + // Source access is inherited by editable copies too. Orphaned sources fail closed. + accessSourceId: uuid("access_source_id"), publicShareToken: text("public_share_token"), forkedFromSkillId: uuid("forked_from_skill_id").references((): AnyPgColumn => companySkills.id, { onDelete: "set null" }), forkedFromCompanyId: uuid("forked_from_company_id").references(() => companies.id, { onDelete: "set null" }), diff --git a/packages/shared/src/types/company-skill.ts b/packages/shared/src/types/company-skill.ts index 1e772f9fa6b..9ab84634f21 100644 --- a/packages/shared/src/types/company-skill.ts +++ b/packages/shared/src/types/company-skill.ts @@ -55,6 +55,7 @@ export interface CompanySkill { authorName: string | null; homepageUrl: string | null; categories: string[]; + accessSourceId?: string | null; sharingScope: CompanySkillSharingScope; publicShareToken: string | null; forkedFromSkillId: string | null; @@ -89,6 +90,7 @@ export interface CompanySkillListItem { authorName: string | null; homepageUrl: string | null; categories: string[]; + accessSourceId?: string | null; sharingScope: CompanySkillSharingScope; publicShareToken: string | null; forkedFromSkillId: string | null; diff --git a/packages/shared/src/types/skill-source.ts b/packages/shared/src/types/skill-source.ts index da457304fb1..87b64ff95c4 100644 --- a/packages/shared/src/types/skill-source.ts +++ b/packages/shared/src/types/skill-source.ts @@ -67,6 +67,12 @@ export interface SkillSource { connectionId: string | null; excludedFolders: string[]; enabled: boolean; + automaticUpdates?: boolean; + importNewSkills?: boolean; + accessScope?: 'company' | 'projects'; + trackingActive?: boolean; + syncState?: 'pending' | 'refreshing' | 'ready' | 'error' | 'paused'; + projects?: Array<{ id: string; name: string; visibility: 'open' | 'private'; active: boolean }>; revision: number; lastAttemptAt: Date | null; lastSuccessAt: Date | null; @@ -74,6 +80,24 @@ export interface SkillSource { lastError: string | null; entries: SkillSourceEntry[]; } +export interface SkillSourceSettingsRequest { + revision: number; + automaticUpdates?: boolean; + importNewSkills?: boolean; + connectionId?: string | null; +} +export interface ProjectSkillRepository { + projectId: string; + projectName: string; + projectVisibility: 'open' | 'private'; + workspaceId: string; + repositoryUrl: string; + trackingRef: string; + bindingId: string | null; + sourceId: string | null; + status: 'available' | 'pending' | 'ready' | 'error'; + lastError: string | null; +} export interface SkillSourceDiscoveryRequest { packageMode?: SkillSourcePackageMode; repositoryUrl: string; diff --git a/packages/shared/src/validators/skill-source.ts b/packages/shared/src/validators/skill-source.ts index 9f9c7714a6a..bd441270fcb 100644 --- a/packages/shared/src/validators/skill-source.ts +++ b/packages/shared/src/validators/skill-source.ts @@ -25,6 +25,15 @@ export const skillSourceSelectionSchema = z.object({ excludedFolders: z.array(repoPath).max(10000), connectionId: z.string().uuid().nullable().optional(), }); +export const skillSourceSettingsSchema = z.object({ + revision: z.number().int().nonnegative(), + automaticUpdates: z.boolean().optional(), + importNewSkills: z.boolean().optional(), + connectionId: z.string().uuid().nullable().optional(), +}); +export const projectSkillSourceAdoptSchema = z.object({ + workspaceId: z.string().uuid(), +}); export const skillSourceDisconnectSchema = z.object({ revision: z.number().int().nonnegative(), diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index 2e8456d3a80..1f23e99d0e9 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -2422,7 +2422,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const saved = await f.management.configuration(f.endpoint.id, "owner-user"); await f.management.saveConfiguration(f.endpoint.id, { expectedRevision: saved.revision, configuration: { ...saved.configuration, defaults: { ...saved.configuration.defaults, - instructions: `Use ${ref(common)} and ${ref(foreign)}.`, + instructions: `Use ${ref(common)}.`, prompts: { ...saved.configuration.defaults.prompts, mention: `Use ${ref(mention)}.`, opened: ref(injected) }, }, } }, "owner-user"); @@ -2452,6 +2452,15 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(await resolveRunScopedMentionedSkillKeys(args)).toEqual([common.key, mention.key]); expect(await resolveRunScopedMentionedSkillKeys({ ...args, commentIds: all.map((c) => c.id) })).toEqual(expect.arrayContaining([common.key, mention.key, next.key])); expect(await resolveRunScopedMentionedSkillKeys({ ...args, commentIds: [] })).toEqual([next.key]); + const invalid = await f.management.configuration(f.endpoint.id, "owner-user"); + await f.management.saveConfiguration(f.endpoint.id, { expectedRevision: invalid.revision, configuration: { + ...invalid.configuration, defaults: { ...invalid.configuration.defaults, instructions: ref(foreign) }, + } }, "owner-user"); + // Instructions are captured on admission; changing configuration must + // not change the already admitted wake above. + await deliverMessage({ callbacks: f.callbacks, endpointId: f.endpoint.id, provider: "github", thread, + trigger: "subscribed_message", message: makeMessage({ id: "41903", text: 'Check again', userId: "42", userName: "octocat" }) }); + await expect(resolveRunScopedMentionedSkillKeys({ ...args, commentIds: [] })).rejects.toThrow("referenced skill is unavailable"); }); it.each([ "github:paperclipai/paperclip:issue:419", diff --git a/server/src/__tests__/codex-local-execute.test.ts b/server/src/__tests__/codex-local-execute.test.ts index 29a3a2b2255..4420280bbef 100644 --- a/server/src/__tests__/codex-local-execute.test.ts +++ b/server/src/__tests__/codex-local-execute.test.ts @@ -1590,7 +1590,8 @@ process.exit(1); await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(sourceHome, { recursive: true }); await fs.mkdir(skillSource, { recursive: true }); - await fs.writeFile(path.join(sourceHome, "auth.json"), fakeCodexAuthJson); + const selectedAuth = JSON.stringify({ tokens: { account_id: "original-selected-account", access_token: "test-subscription-token" } }); + await fs.writeFile(path.join(sourceHome, "auth.json"), selectedAuth); await fs.writeFile(path.join(skillSource, "SKILL.md"), "# AgentMail\nAssigned inbox one."); await writeFakeCodexCommand(command); const keys = ["PAPERCLIP_HOME", "PAPERCLIP_INSTANCE_ID", "CODEX_HOME"] as const; @@ -1634,8 +1635,127 @@ process.exit(1); await expect(fs.stat(path.join(sourceHome, "skills/agentmail"))).rejects.toMatchObject({ code: "ENOENT" }); const removed = await invoke("agent-1", null); expect(removed.prompt).not.toContain("inline@example.test"); - expect(removed.codexHome).toBe(sourceHome); + expect(removed.codexHome).not.toBe(sourceHome); + expect(removed.codexHome).toContain("skill-runtimes/agent-1/"); await expect(fs.stat(path.join(removed.codexHome!, "skills/agentmail"))).rejects.toMatchObject({ code: "ENOENT" }); + // Remote credential copy-back leaves a regular file. A later authorized + // account switch must not reuse that previous account's generated home. + await fs.unlink(path.join(first.codexHome!, "auth.json")); + await fs.writeFile(path.join(first.codexHome!, "auth.json"), selectedAuth); + const nextAccount = JSON.parse(selectedAuth); + nextAccount.tokens.account_id = "new-selected-account"; + await fs.writeFile(path.join(sourceHome, "auth.json"), JSON.stringify(nextAccount)); + const switched = await invoke("agent-1", "a".repeat(64)); + expect(switched.codexHome).not.toBe(first.codexHome); + expect(JSON.parse(await fs.readFile(path.join(switched.codexHome!, "auth.json"), "utf8")).tokens.account_id).toBe("new-selected-account"); + nextAccount.tokens.access_token = "refreshed-same-account-token"; + await fs.writeFile(path.join(sourceHome, "auth.json"), JSON.stringify(nextAccount)); + expect((await invoke("agent-1", "a".repeat(64))).codexHome).toBe(switched.codexHome); + expect(await fs.readFile(path.join(first.codexHome!, "auth.json"), "utf8")).toBe(selectedAuth); + + } finally { + for (const key of keys) { if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key]; } + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it.each([false, true])("isolates private skills and keeps custom routing with an explicit credential home: %s", async (explicitHome) => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-private-codex-")); + const workspace = path.join(root, "workspace"); + const command = path.join(root, "codex"); + const capture = path.join(root, "capture.json"); + const sourceHome = path.join(root, "selected-account"); + const skillSource = path.join(root, "skill-v1"); + await fs.mkdir(workspace, { recursive: true }); + await fs.mkdir(sourceHome, { recursive: true }); + await fs.mkdir(skillSource, { recursive: true }); + await fs.writeFile(path.join(sourceHome, "auth.json"), fakeCodexAuthJson); + await fs.writeFile(path.join(skillSource, "SKILL.md"), "# AgentMail\nAssigned inbox one."); + await writeFakeCodexCommand(command); + const keys = ["PAPERCLIP_HOME", "PAPERCLIP_INSTANCE_ID", "CODEX_HOME"] as const; + const previous = Object.fromEntries(keys.map((key) => [key, process.env[key]])); + process.env.PAPERCLIP_HOME = path.join(root, "paperclip"); + process.env.PAPERCLIP_INSTANCE_ID = "connectors"; + process.env.CODEX_HOME = sourceHome; + const invoke = async (agentId: string, digest: string | null, source = skillSource, connectorSkillInstructions = "") => { + const config = { + engine: "cli", command, cwd: workspace, + env: { ...(explicitHome ? { CODEX_HOME: sourceHome } : {}), PAPERCLIP_TEST_CAPTURE_PATH: capture, + PAPERCLIP_CODEX_PROVIDERS: JSON.stringify({ providers: { test_gateway: { + name: "Fixture gateway", base_url: "https://gateway.example/v1", wire_api: "responses", + } }, model_provider: "test_gateway" }), + }, + + paperclipSkillSync: { desiredSkills: digest ? ["paperclipai/paperclip/agentmail"] : [] }, + paperclipRuntimeSkills: digest ? [{ key: "paperclipai/paperclip/agentmail", runtimeName: "agentmail", source }] : [], + }; + const result = await execute({ runId: `run-${agentId}-${digest?.slice(0, 1) ?? "none"}`, + agent: { id: agentId, companyId: "company-1", name: "Email agent", adapterType: "codex_local", adapterConfig: config }, + runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null }, + config, context: { paperclipWake: { connectorSkillInstructions } }, authToken: "test-token", onLog: async () => {}, + }); + expect(result.errorMessage).toBeNull(); + expect(result.exitCode).toBe(0); + return JSON.parse(await fs.readFile(capture, "utf8")) as CapturePayload; + }; + try { + const first = await invoke("agent-1", "a".repeat(64)); + expect(first.codexHome).toContain("skill-runtimes/agent-1/"); + expect(first.codexConfigContents).toContain('model_provider = "test_gateway"'); + expect(first.codexConfigContents).toContain('base_url = "https://gateway.example/v1"'); + expect(await fs.realpath(path.join(first.codexHome!, "auth.json"))).toBe(await fs.realpath(path.join(sourceHome, "auth.json"))); + expect(await fs.readFile(path.join(first.codexHome!, "skills/agentmail/SKILL.md"), "utf8")).toContain("inbox one"); + await expect(fs.stat(path.join(sourceHome, "skills/agentmail"))).rejects.toMatchObject({ code: "ENOENT" }); + const unchanged = await invoke("agent-1", "a".repeat(64)); + expect(unchanged.codexHome).toBe(first.codexHome); + const other = await invoke("agent-2", "a".repeat(64)); + expect(other.codexHome).not.toBe(first.codexHome); + const nextSource = path.join(root, "skill-v2"); + await fs.mkdir(nextSource); + await fs.writeFile(path.join(nextSource, "SKILL.md"), "# AgentMail\nAssigned inbox two."); + const next = await invoke("agent-1", "b".repeat(64), nextSource); + expect(next.codexHome).not.toBe(first.codexHome); + expect(await fs.readFile(path.join(next.codexHome!, "skills/agentmail/SKILL.md"), "utf8")).toContain("inbox two"); + const inline = await invoke("agent-1", null, skillSource, "# AgentMail\nAssigned inbox inline@example.test"); + expect(inline.prompt).toContain("Assigned inbox inline@example.test"); + await expect(fs.stat(path.join(sourceHome, "skills/agentmail"))).rejects.toMatchObject({ code: "ENOENT" }); + const removed = await invoke("agent-1", null); + expect(removed.prompt).not.toContain("inline@example.test"); + // The old saved files still exist; exclusion is enforced by the launch home. + expect(await fs.readFile(path.join(first.codexHome!, "skills/agentmail/SKILL.md"), "utf8")).toContain("inbox one"); + expect(removed.codexHome).not.toBe(first.codexHome); + expect(removed.codexConfigContents).toContain('model_provider = "test_gateway"'); + expect(await fs.realpath(path.join(removed.codexHome!, "auth.json"))).toBe(await fs.realpath(path.join(sourceHome, "auth.json"))); + expect(removed.codexHome).not.toBe(sourceHome); + expect(removed.codexHome).toContain("skill-runtimes/agent-1/"); + await expect(fs.stat(path.join(removed.codexHome!, "skills/agentmail"))).rejects.toMatchObject({ code: "ENOENT" }); + if (!explicitHome) { + // A promoted company login can differ from the host's default account. + // Digest and seeding must use that same effective credential source. + const companyHome = path.resolve(first.codexHome!, "../../.."); + await fs.unlink(path.join(companyHome, "auth.json")); + const originalAccount = JSON.stringify({ tokens: { account_id: "company-account-one", access_token: "test-token-one" } }); + await fs.writeFile(path.join(companyHome, "auth.json"), originalAccount); + const companyRun = await invoke("agent-1", "a".repeat(64)); + expect(JSON.parse(await fs.readFile(path.join(companyRun.codexHome!, "auth.json"), "utf8")).tokens.account_id).toBe("company-account-one"); + await fs.unlink(path.join(companyRun.codexHome!, "auth.json")); + await fs.writeFile(path.join(companyRun.codexHome!, "auth.json"), originalAccount); + await fs.writeFile(path.join(companyHome, "auth.json"), JSON.stringify({ tokens: { account_id: "company-account-two", access_token: "test-token-two" } })); + const companyNext = await invoke("agent-1", "a".repeat(64)); + expect(companyNext.codexHome).not.toBe(companyRun.codexHome); + expect(JSON.parse(await fs.readFile(path.join(companyNext.codexHome!, "auth.json"), "utf8")).tokens.account_id).toBe("company-account-two"); + expect(await fs.readFile(path.join(sourceHome, "auth.json"), "utf8")).toBe(fakeCodexAuthJson); + } else { + await fs.writeFile(path.join(sourceHome, "config.toml"), '[mcp_servers.changed]\ncommand = "new-selected-server"\n'); + const settingsChanged = await invoke("agent-1", "a".repeat(64)); + expect(settingsChanged.codexHome).not.toBe(first.codexHome); + expect(settingsChanged.codexConfigContents).toContain('command = "new-selected-server"'); + expect(settingsChanged.codexConfigContents).toContain('model_provider = "test_gateway"'); + await fs.unlink(path.join(sourceHome, "config.toml")); + const settingsRemoved = await invoke("agent-1", "a".repeat(64)); + expect(settingsRemoved.codexConfigContents).not.toContain('new-selected-server'); + } + } finally { for (const key of keys) { if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key]; } await fs.rm(root, { recursive: true, force: true }); diff --git a/server/src/__tests__/company-skills-routes.test.ts b/server/src/__tests__/company-skills-routes.test.ts index 4b2d670db53..18e395ec232 100644 --- a/server/src/__tests__/company-skills-routes.test.ts +++ b/server/src/__tests__/company-skills-routes.test.ts @@ -3,6 +3,8 @@ import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { hoistModuleGraph } from "./helpers/hoist-module-graph.js"; +const mockCaptureRunIdentity = vi.hoisted(() => vi.fn()); + const mockSkillSourceService = vi.hoisted(() => ({ sourceForSkill: vi.fn(), importFromUrl: vi.fn(), discover: vi.fn() })); const mockAgentService = vi.hoisted(() => ({ @@ -133,6 +135,10 @@ function denySkillPolicy(action = "skills.import") { } function registerModuleMocks() { + vi.doMock("../services/run-identity.js", async () => ({ + ...await vi.importActual("../services/run-identity.js"), + captureRunIdentity: mockCaptureRunIdentity, + })); vi.doMock("../services/skill-sources.js", () => ({ skillSourceService: () => mockSkillSourceService })); vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js")); @@ -159,6 +165,15 @@ function registerModuleMocks() { agentService: () => mockAgentService, })); + // This route-contract harness supplies no database. Private-source decisions + // are exercised with the real database in project-skill-sources.test.ts. + vi.doMock("../services/skill-access.js", async () => { + const actual = await vi.importActual("../services/skill-access.js"); + return { ...actual, skillAccessService: (db: Parameters[0]) => ({ + ...actual.skillAccessService(db), assertSource: async () => {}, + }) }; + }); + vi.doMock("../services/company-skills.js", async () => { const actual = await vi.importActual( "../services/company-skills.js", @@ -295,6 +310,7 @@ describe("company skill mutation permissions", () => { beforeEach(() => { vi.clearAllMocks(); + mockCaptureRunIdentity.mockResolvedValue({ run: { responsibleUserId: "validated-owner" } }); mockSkillSourceService.sourceForSkill.mockResolvedValue(null); mockSkillSourceService.importFromUrl.mockImplementation((companyId, source) => mockCompanySkillService.importFromSource(companyId, source)); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); @@ -1232,6 +1248,33 @@ describe("company skill mutation permissions", () => { }); }); + it.each(["responsible-person", "local-board"])("does not persist API-key refresh authority from %s", async (onBehalfOfUserId) => { + const agent = { ...assignedAgentActor(), source: "agent_key", runId: undefined, onBehalfOfUserId, + onBehalfOfMemberships: [{ companyId: "company-1", membershipRole: "member", status: "active" }] }; + const response = await request(createApp(agent)).post("/api/companies/company-1/skills/import") + .send({ source: "https://github.com/acme/public-skills" }); + expect(response.status, JSON.stringify(response.body)).toBe(201); + expect(mockSkillSourceService.importFromUrl).toHaveBeenCalledWith("company-1", expect.anything(), + expect.objectContaining({ setup: { userId: null, agentId: agent.agentId, localTrusted: false } })); + }); + + it("derives refresh authority from the authenticated run instead of caller responsibility", async () => { + const agent = { ...assignedAgentActor(), source: "agent_jwt", onBehalfOfUserId: "unverified-person", + onBehalfOfMemberships: [{ companyId: "company-1", membershipRole: "member", status: "active" }] }; + const response = await request(createApp(agent)).post("/api/companies/company-1/skills/import") + .send({ source: "https://github.com/acme/public-skills" }); + expect(response.status, JSON.stringify(response.body)).toBe(201); + expect(mockCaptureRunIdentity).toHaveBeenCalledWith(expect.anything(), { companyId: "company-1", agentId: agent.agentId, runId: "run-1" }); + expect(mockSkillSourceService.importFromUrl).toHaveBeenCalledWith("company-1", expect.anything(), + expect.objectContaining({ setup: { userId: "validated-owner", agentId: agent.agentId, localTrusted: false } })); + }); + + it("rejects a run-authenticated import without a run identity", async () => { + await request(createApp({ ...assignedAgentActor(), source: "agent_jwt", runId: undefined })) + .post("/api/companies/company-1/skills/import").send({ source: "https://github.com/acme/public-skills" }).expect(403); + expect(mockSkillSourceService.importFromUrl).not.toHaveBeenCalled(); + }); + it('streams discovery updates and a final result while retaining JSON compatibility', async () => { const result = { candidates: [], commitSha: 'a'.repeat(40) }; mockSkillSourceService.discover.mockImplementation(async (_input, _context, options) => { diff --git a/server/src/__tests__/github-skill-source.test.ts b/server/src/__tests__/github-skill-source.test.ts index b45f64303b6..c5c93def05f 100644 --- a/server/src/__tests__/github-skill-source.test.ts +++ b/server/src/__tests__/github-skill-source.test.ts @@ -6,13 +6,38 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { describe, expect, it, vi } from 'vitest'; -import { scanGitHubSkills, previewGitHubSkillFile, parseSkillRepository } from '../services/github-skill-source.js'; +import { scanGitHubSkills, scanGitHubSkillsWithPackageFallback, rejectIncompleteSkillPackages, previewGitHubSkillFile, parseSkillRepository } from '../services/github-skill-source.js'; import { skillSourceDiscoverySchema, skillSourcePreviewSchema, type SkillSourceScanUpdate } from '@paperclipai/shared'; import { skillFileBytes } from '../services/skill-snapshot.js'; const sha = 'a'.repeat(40); const md = (name: string) => `---\nname: ${name}\ndescription: A useful skill\n---\nFollow these instructions.\n`; describe('GitHub skill repository discovery', () => { + it('keeps bounded packages discoverable but rejects dependencies omitted by repository fallback', async () => { + const fixture = githubFixture({ + 'skills/main/SKILL.md': md('main') + '\nRead [shared](../../shared/guide.md) and [helper](../helper/SKILL.md).', + 'skills/helper/SKILL.md': md('helper'), + 'shared/guide.md': 'Shared instructions', + ...Object.fromEntries(Array.from({ length: 62 }, (_, i) => [`generated/${i}.bin`, 'x'])), + }); + const open = fixture.openSnapshot; + fixture.openSnapshot = async (...args) => { + const snapshot = await open(...args); + for (const entry of snapshot.entries) if (entry.path.startsWith('generated/')) entry.size = 1024 * 1024; + return snapshot; + }; + const scan = await scanGitHubSkillsWithPackageFallback({ repositoryUrl: 'https://github.com/acme/skills', packageMode: 'repository' }, fixture); + expect(scan.packageMode).toBe('skills'); + expect(scan.skills.find(skill => skill.name === 'helper')?.error).toBeNull(); + expect(scan.skills.find(skill => skill.name === 'main')?.error).toContain('dependencies could not be preserved'); + expect(scan.candidates.find(skill => skill.name === 'main')?.error).toContain('dependencies could not be preserved'); + const later = await scanGitHubSkills({ repositoryUrl: 'https://github.com/acme/skills', packageMode: 'skills' }, fixture); + expect(later.skills.find(skill => skill.name === 'main')?.error).toBeNull(); + rejectIncompleteSkillPackages(later); + expect(later.candidates.find(skill => skill.name === 'main')?.error).toContain('dependencies could not be preserved'); + expect(later.skills.find(skill => skill.name === 'helper')?.error).toBeNull(); + }); + it('resolves moving branches before cache lookup and reuses the same pinned commit until the branch changes', async () => { const fixture = githubFixture({ 'SKILL.md': md('one') }); const read = vi.fn(fixture) as unknown as typeof fixture; diff --git a/server/src/__tests__/heartbeat-execution-configuration.test.ts b/server/src/__tests__/heartbeat-execution-configuration.test.ts index ee1dbd66298..7f1a96e01ea 100644 --- a/server/src/__tests__/heartbeat-execution-configuration.test.ts +++ b/server/src/__tests__/heartbeat-execution-configuration.test.ts @@ -1,9 +1,11 @@ import { randomUUID } from "node:crypto"; -import { access, readFile } from "node:fs/promises"; +import { access, readFile, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import path from "node:path"; import { eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { agents, companies, companyMemberships, createDb, heartbeatRunEvents, heartbeatRuns, issues, projects } from "@paperclipai/db"; +import { agents, companies, companyMemberships, companySkills, createDb, heartbeatRunEvents, heartbeatRuns, issues, projects } from "@paperclipai/db"; +import { companySkillService } from "../services/company-skills.js"; import { registerServerAdapter, unregisterServerAdapter, type AdapterExecutionContext } from "../adapters/index.js"; import { HttpError } from "../errors.js"; import { heartbeatService } from "../services/heartbeat.js"; @@ -37,10 +39,14 @@ describePostgres("heartbeat execution configuration through adapter dispatch", ( const captured = new Map(); const inspect = new Map Promise>(); const adapterType = "codex_local"; + const oldHome = process.env.PAPERCLIP_HOME; + let home: string; beforeAll(async () => { temporary = await startEmbeddedPostgresTestDatabase("heartbeat-execution-config-"); db = createDb(temporary.connectionString); + home = await mkdtemp(path.join(tmpdir(), "paperclip-execution-skills-")); + process.env.PAPERCLIP_HOME = home; heartbeat = heartbeatService(db); // Exercise real preparation for a managed-capable adapter without calling a provider. registerServerAdapter({ @@ -60,6 +66,8 @@ describePostgres("heartbeat execution configuration through adapter dispatch", ( unregisterServerAdapter(adapterType); await db?.$client.end({ timeout: 0 }); await temporary?.cleanup(); + if (oldHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = oldHome; + if (home) await rm(home, { recursive: true, force: true }); }); async function seed(managed = false) { @@ -157,4 +165,37 @@ describePostgres("heartbeat execution configuration through adapter dispatch", ( const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, f.agentId)); expect(runs.filter(run => run.id !== finished.id)).toEqual([expect.objectContaining({ status: "scheduled_retry", scheduledRetryReason: "ai_connection_busy" })]); }); + + it("snapshots only selected immutable skills and omits inaccessible optional assignments", async () => { + const f = await seed(); + const skills = companySkillService(db); + const selected = await skills.createLocalSkill(f.companyId, { name: "Selected workflow", markdown: "# Selected\nUse the original workflow.\n" }); + const unused = await skills.createLocalSkill(f.companyId, { name: "Unused workflow" }); + const restricted = await skills.createLocalSkill(f.companyId, { name: "Restricted workflow" }); + await db.update(companySkills).set({ accessSourceId: randomUUID() }).where(eq(companySkills.id, restricted.id)); + await db.update(agents).set({ adapterConfig: { paperclipSkillSync: { desiredSkills: [restricted.key] } } }).where(eq(agents.id, f.agentId)); + await db.update(issues).set({ description: "Use [/selected](skill://" + selected.id + "?s=selected-workflow)." }).where(eq(issues.id, f.issueId)); + const { finished, input } = await execute(f); + expect(finished.status, finished.error ?? undefined).toBe("succeeded"); + const inventory = input?.config.paperclipRuntimeSkills as Array<{ key: string; source: string; versionId: string }>; + expect(inventory.some(entry => entry.key === unused.key || entry.key === restricted.key)).toBe(false); + const delivered = inventory.find(entry => entry.key === selected.key)!; + expect(delivered.versionId).toBe(selected.currentVersionId); + expect(delivered.source).toContain(selected.currentVersionId!); + expect(await readFile(path.join(delivered.source, "SKILL.md"), "utf8")).toContain("Use the original workflow."); + expect(finished.contextSnapshot).toMatchObject({ skillSelectionSnapshot: true, skillVersions: { [selected.key]: selected.currentVersionId } }); + expect(Object.keys(finished.contextSnapshot!.skillVersions as object)).not.toContain(unused.key); + expect(Object.keys(finished.contextSnapshot!.skillVersions as object)).not.toContain(restricted.key); + }); + + it("rejects an inaccessible explicit skill reference before adapter dispatch", async () => { + const f = await seed(); + const skill = await companySkillService(db).createLocalSkill(f.companyId, { name: "Private reference" }); + await db.update(companySkills).set({ accessSourceId: randomUUID() }).where(eq(companySkills.id, skill.id)); + await db.update(issues).set({ description: "Use [/private](skill://" + skill.id + "?s=private-reference)." }).where(eq(issues.id, f.issueId)); + const { finished, input } = await execute(f); + expect(finished.status).toBe("failed"); + expect(finished.error).toContain("referenced skill is unavailable"); + expect(input).toBeUndefined(); + }); }); diff --git a/server/src/__tests__/heartbeat-runtime-skills.test.ts b/server/src/__tests__/heartbeat-runtime-skills.test.ts index 97e2d6b59ed..247e7069bbb 100644 --- a/server/src/__tests__/heartbeat-runtime-skills.test.ts +++ b/server/src/__tests__/heartbeat-runtime-skills.test.ts @@ -8,6 +8,8 @@ import { agents, companies, companySkills, + projects, + projectWorkspaces, createDb, toolApplications, toolConnectionInstalls, @@ -110,9 +112,10 @@ describeEmbeddedPostgres("heartbeat runtime skill version pins", () => { }, 20_000); afterEach(async () => { + // Terminal status precedes trailing writes; drain them before truncating the fixture. + await heartbeatService(db).drainActiveRunExecutions(); capturedRuns.length = 0; await instanceSettingsService(db).updateExperimental({ enableBetaSkills: false }); - await new Promise((resolve) => setTimeout(resolve, 100)); await db.execute(sql.raw(` TRUNCATE TABLE "activity_log", @@ -144,6 +147,39 @@ describeEmbeddedPostgres("heartbeat runtime skill version pins", () => { await tempDb?.cleanup(); }); + it.each(["local_path", "catalog"] as const)("snapshots changed installed %s files for new executions and keeps resume versions", async (sourceType) => { + const companyId = randomUUID(); + await db.insert(companies).values({ id: companyId, name: 'Local skill snapshots', issuePrefix: 'LSK' }); + const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'paperclip-local-snapshot-')); + cleanupDirs.add(directory); + const [project] = await db.insert(projects).values({ companyId, name: 'Approved local workspace' }).returning(); + await db.insert(projectWorkspaces).values({ companyId, projectId: project!.id, name: 'Local skill workspace', sourceType: 'local_path', cwd: directory, isPrimary: true }); + await fs.mkdir(path.join(directory, 'references')); + await fs.writeFile(path.join(directory, 'SKILL.md'), '---\nname: local-snapshot\ndescription: Read the installed reference.\n---\nRead references/marker.txt.\n'); + await fs.writeFile(path.join(directory, 'references/marker.txt'), 'v1\n'); + const skills = companySkillService(db); + const imported = await skills.importFromSource(companyId, directory); + const key = imported.imported[0]!.key; + if (sourceType === "catalog") await db.update(companySkills).set({ sourceType }).where(eq(companySkills.key, key)); + const selected = { snapshotLatest: true, onlyKeys: new Set([key]) }; + const [first] = await skills.listRuntimeSkillEntries(companyId, selected); + expect(first!.versionId).toBeTruthy(); + expect(await fs.readFile(path.join(first!.source, 'references/marker.txt'), 'utf8')).toBe('v1\n'); + + // A reference-only change must be discovered even when SKILL.md is unchanged. + await fs.writeFile(path.join(directory, 'references/marker.txt'), 'v2\n'); + if (sourceType === "local_path") await skills.importFromSource(companyId, directory); + const [latest] = await skills.listRuntimeSkillEntries(companyId, selected); + expect(latest!.versionId).not.toBe(first!.versionId); + expect(await fs.readFile(path.join(latest!.source, 'references/marker.txt'), 'utf8')).toBe('v2\n'); + const [unchanged] = await skills.listRuntimeSkillEntries(companyId, selected); + expect(unchanged!.versionId).toBe(latest!.versionId); + + const [resumed] = await skills.listRuntimeSkillEntries(companyId, { ...selected, versionSelections: new Map([[key, first!.versionId!]]) }); + expect(resumed!.versionId).toBe(first!.versionId); + expect(await fs.readFile(path.join(resumed!.source, 'references/marker.txt'), 'utf8')).toBe('v1\n'); + }); + it("materializes different pinned skill versions for different agents at runtime", async () => { const companyId = randomUUID(); const skillId = randomUUID(); @@ -292,7 +328,7 @@ describeEmbeddedPostgres("heartbeat runtime skill version pins", () => { ?.skills.find((entry) => entry.key === skillKey); expect(defaultSkill).toMatchObject({ key: skillKey, - versionId: null, + versionId: versionTwo.id, currentVersionId: versionTwo.id, sourceStatus: "available", }); diff --git a/server/src/__tests__/helpers/github-skills.ts b/server/src/__tests__/helpers/github-skills.ts index dd0fcec6a3e..601054e2a64 100644 --- a/server/src/__tests__/helpers/github-skills.ts +++ b/server/src/__tests__/helpers/github-skills.ts @@ -1,15 +1,17 @@ +import { createHash } from "node:crypto"; import type { GitHubRead } from "../../services/github-skill-source.js"; export function githubFixture(files: Record, modes: Record = {}, commit = "a".repeat(40)): GitHubRead { const entries = Object.keys(files).map((path, i) => ({ path, type: modes[path] === '160000' ? 'commit' : 'blob', mode: modes[path] ?? '100644', sha: String(i), size: Buffer.byteLength(files[path]!) })); + const treeSha = createHash("sha1").update(JSON.stringify(entries.map(entry => [entry.path, entry.mode, files[entry.path]]).sort())).digest("hex"); const read = (async (url: string) => { if (url === '/repos/acme/skills') return { id: 42, full_name: 'acme/skills', default_branch: 'main' }; - if (url.startsWith('/repos/acme/skills/commits/')) return { sha: commit }; + if (url.startsWith('/repos/acme/skills/commits/')) return { sha: commit, commit: { tree: { sha: treeSha } } }; throw new Error(`Unexpected request ${url}`); }) as GitHubRead; read.openSnapshot = async (_input, options) => { options?.signal?.throwIfAborted(); await options?.onDownload?.({ stage: 'receiving', percent: 50, receivedBytes: 1024 }); - return { commitSha: commit, entries, readBlob: async entry => Buffer.from(files[entry.path]!), release: async () => {} }; + return { commitSha: commit, treeSha, downloadBytes: entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0), entries, readBlob: async entry => Buffer.from(files[entry.path]!), release: async () => {} }; }; return read; } diff --git a/server/src/__tests__/project-routes-env.test.ts b/server/src/__tests__/project-routes-env.test.ts index 25072e1491e..7f929c23436 100644 --- a/server/src/__tests__/project-routes-env.test.ts +++ b/server/src/__tests__/project-routes-env.test.ts @@ -170,6 +170,16 @@ describe("project env routes", () => { mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env); }); + it('requires workspace-write authority to attach a repository despite project read access', async () => { + mockProjectService.getById.mockResolvedValue(buildProject()); + mockAccessService.decide.mockImplementation(async (input) => ({ allowed: input.action !== 'project:write_workspace', + action: input.action, reason: 'deny_no_grant', explanation: 'Workspace write permission required.' })); + const app = await createApp(); + await request(app).post('/api/projects/project-1/workspaces').send({ name: 'Repository', repoUrl: 'https://github.com/acme/skills' }).expect(403); + await request(app).put('/api/projects/project-1/repositories').send({ repositoryIds: ['42'] }).expect(403); + expect(mockProjectService.createWorkspace).not.toHaveBeenCalled(); + }); + it("normalizes env bindings on create and logs only env keys", async () => { const normalizedEnv = { API_KEY: { diff --git a/server/src/__tests__/project-skill-setup-identity.test.ts b/server/src/__tests__/project-skill-setup-identity.test.ts new file mode 100644 index 00000000000..844c4f7df71 --- /dev/null +++ b/server/src/__tests__/project-skill-setup-identity.test.ts @@ -0,0 +1,25 @@ +import { beforeEach, expect, it, vi } from 'vitest'; +import type { Db } from '@paperclipai/db'; +import { projectSkillSetupIdentity } from '../services/project-skill-source-bindings.js'; +const { capture } = vi.hoisted(() => ({ capture: vi.fn() })); +vi.mock('../services/run-identity.js', () => ({ captureRunIdentity: capture })); +const db = {} as Db; +const actor = { type: 'agent' as const, source: 'agent_jwt' as const, companyId: 'company', agentId: 'agent', runId: 'run', onBehalfOfUserId: 'forged' }; +beforeEach(() => capture.mockReset()); +it('keeps responsibility without searching personal GitHub grants when the run has no selected credential', async () => { + capture.mockResolvedValue({ run: { responsibleUserId: 'person' }, context: { github: null } }); + expect(await projectSkillSetupIdentity(db, 'company', actor)).toMatchObject({ refreshUserId: 'person', connectionId: null, refreshGrantId: null }); +}); +it('pins only the connection and grant already authorized for the active run', async () => { + capture.mockResolvedValue({ run: { responsibleUserId: 'person' }, context: { github: { status: 'available', connectionId: 'selected', grantId: 'grant' } } }); + expect(await projectSkillSetupIdentity(db, 'company', actor)).toMatchObject({ refreshUserId: 'person', connectionId: 'selected', refreshGrantId: 'grant' }); + expect(capture).toHaveBeenCalledWith(db, { companyId: 'company', agentId: 'agent', runId: 'run' }); +}); +it('does not reuse an unavailable run credential', async () => { + capture.mockResolvedValue({ run: { responsibleUserId: 'person' }, context: { github: { status: 'unavailable', connectionId: 'old', grantId: 'old' } } }); + expect(await projectSkillSetupIdentity(db, 'company', actor)).toMatchObject({ connectionId: null, refreshGrantId: null }); +}); +it('API keys do not obtain responsibility or a run credential', async () => { + expect(await projectSkillSetupIdentity(db, 'company', { ...actor, source: 'agent_key' })).toMatchObject({ refreshUserId: null, connectionId: null, refreshGrantId: null }); + expect(capture).not.toHaveBeenCalled(); +}); diff --git a/server/src/__tests__/project-skill-sources.test.ts b/server/src/__tests__/project-skill-sources.test.ts new file mode 100644 index 00000000000..fcaaf791a0b --- /dev/null +++ b/server/src/__tests__/project-skill-sources.test.ts @@ -0,0 +1,893 @@ +import { reserveInitialSkillWork } from '../services/skill-source-import-budget.js'; +import { admitDeferredSkillImports, lockSkillImportQueue, MAX_COMPANY_PENDING_SKILL_IMPORTS, MAX_PROJECT_PENDING_SKILL_IMPORTS, SKILL_IMPORT_CAPACITY_WAIT } from '../services/skill-source-import-queue.js'; +import { randomUUID } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { and, eq, sql } from 'drizzle-orm'; +import { activityLog, agents, authUsers, companies, companyMemberships, companySkillSources, companySkillImportBudgets, companySkills, createDb, projects, + projectWorkspaces, projectSkillSources, projectAccessMembers } from '@paperclipai/db'; +import { startEmbeddedPostgresTestDatabase, getEmbeddedPostgresTestSupport } from './helpers/embedded-postgres.js'; +import { githubFixture } from './helpers/github-skills.js'; +import { enqueueProjectSkillSource, detachProjectSkillSource, projectSkillSetupIdentity } from '../services/project-skill-source-bindings.js'; +import { adoptProjectSkillRepository, projectSkillRepositories, skillSourceSyncService } from '../services/skill-source-sync.js'; +import { skillSourceService, type SkillSourceContext } from '../services/skill-sources.js'; +import { companySkillService } from '../services/company-skills.js'; +import { projectService } from '../services/projects.js'; +import { skillAccessService, withSkillActor } from '../services/skill-access.js'; +import type { AuthorizationActor } from '../services/authorization.js'; +import { forbidden, unprocessable } from '../errors.js'; + +const support = await getEmbeddedPostgresTestSupport(); +describe.skipIf(!support.supported)('automatic project skill sources', () => { + let testDb: Awaited>; + let db: ReturnType; + let home: string; + const oldHome = process.env.PAPERCLIP_HOME; + const companyId = randomUUID(); + const md = (name: string) => `---\nname: ${name}\ndescription: Test workflow\n---\nFollow the workflow.\n`; + let files: Record = {}; + let commit = 'a'.repeat(40); + const downloads = vi.fn(); + const context: SkillSourceContext = { actor: { type: 'system' }, setup: { userId: null, localTrusted: true }, + read: () => { + const fixture = githubFixture(files, {}, commit), open = fixture.openSnapshot; + fixture.openSnapshot = async (...args) => { downloads(); return open(...args); }; + return fixture; + }, authorize: async () => {}, audit: async () => {} }; + beforeAll(async () => { + testDb = await startEmbeddedPostgresTestDatabase('paperclip-project-skills-'); + db = createDb(testDb.connectionString); + home = await fs.mkdtemp(path.join(os.tmpdir(), 'paperclip-project-skills-home-')); + process.env.PAPERCLIP_HOME = home; + await db.insert(companies).values({ id: companyId, name: 'Project skills', issuePrefix: 'PSK' }); + }, 30000); + it('requires explicit repair before an agent-created repository can search a person’s GitHub grants', async () => { + const row = await workspace('agent-repair', 'open'); + const binding = await enqueueProjectSkillSource(db, row); + const [agent] = await db.insert(agents).values({ companyId, name: 'Agent repository setup', adapterType: 'codex_local', status: 'idle' }).returning(); + await db.update(projectSkillSources).set({ refreshUserId: 'local-board', refreshAgentId: agent!.id, refreshLocalTrusted: true }).where(eq(projectSkillSources.id, binding!.id)); + const provider = vi.spyOn(globalThis, 'fetch'); + try { + await skillSourceSyncService(db).tick(); + const [saved] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, binding!.id)); + expect(saved!.lastError).toMatch(/Choose a GitHub connection/); + expect(saved!.active).toBe(false); + expect(provider).not.toHaveBeenCalled(); + } finally { provider.mockRestore(); } + }); + afterAll(async () => { + if (oldHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = oldHome; + await testDb?.cleanup(); + if (home) await fs.rm(home, { recursive: true, force: true }); + }); + async function workspace(ref: string, visibility = 'open') { + const [project] = await db.insert(projects).values({ companyId, name: ref, visibility }).returning(); + const [row] = await db.insert(projectWorkspaces).values({ companyId, projectId: project!.id, name: ref, + repoUrl: 'https://github.com/acme/skills', defaultRef: ref }).returning(); + return row!; + } + const actor = (userId: string): AuthorizationActor => ({ type: 'board', userId, source: 'session', companyIds: [companyId], ignoreInstanceAdmin: true }); + + it('bounds per-project pending imports and admits deferred work after capacity frees', async () => { + await db.update(projectSkillSources).set({ active: false, lastError: null }); + const root = await workspace('quota-root'); + const rows = []; + for (let index = 0; index <= MAX_PROJECT_PENDING_SKILL_IMPORTS; index++) { + const [row] = await db.insert(projectWorkspaces).values({ companyId, projectId: root.projectId, name: `Repository ${index}`, + repoUrl: root.repoUrl, defaultRef: `project-quota-${index}` }).returning(); + rows.push(row!); await enqueueProjectSkillSource(db, row!); + } + const pending = await db.select().from(projectSkillSources).where(eq(projectSkillSources.projectId, root.projectId)); + expect(pending.filter(row => row.active)).toHaveLength(MAX_PROJECT_PENDING_SKILL_IMPORTS); + expect(pending.filter(row => !row.active)).toEqual([expect.objectContaining({ status: 'pending', lastError: SKILL_IMPORT_CAPACITY_WAIT })]); + await detachProjectSkillSource(db, companyId, rows[0]!.id); + await admitDeferredSkillImports(db, companyId, new Date()); + const [admitted] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, rows.at(-1)!.id)); + expect(admitted).toMatchObject({ active: true, lastError: null }); + const [extra] = await db.insert(projectWorkspaces).values({ companyId, projectId: root.projectId, name: 'Detached waiting repository', repoUrl: root.repoUrl, defaultRef: 'quota-detached' }).returning(); + await enqueueProjectSkillSource(db, extra!); + await detachProjectSkillSource(db, companyId, extra!.id); + await detachProjectSkillSource(db, companyId, rows[1]!.id); + await admitDeferredSkillImports(db, companyId, new Date()); + const [detached] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, extra!.id)); + expect(detached!.active).toBe(false); + await db.update(projectSkillSources).set({ active: false, lastError: null }); + }); + + it('does not reactivate a deferred import detached after admission selection', async () => { + const row = await workspace('deferred-removal-race'); + const binding = await enqueueProjectSkillSource(db, row); + await db.update(projectSkillSources).set({ active: false, lastError: SKILL_IMPORT_CAPACITY_WAIT }).where(eq(projectSkillSources.id, binding!.id)); + let detached = false; + const database = new Proxy(db, { get(target, key) { + if (key !== 'transaction') return Reflect.get(target, key); + return (callback: (tx: unknown) => Promise) => target.transaction(async tx => callback(new Proxy(tx, { get(target, key) { + if (key !== 'update') return Reflect.get(target, key); + return (table: unknown) => { + const update = target.update(table as typeof projectSkillSources); + if (table !== projectSkillSources) return update; + return { set: (values: Parameters[0]) => { + const set = update.set(values); + return { where: async (condition: Parameters[0]) => { + if (!detached) { detached = true; await detachProjectSkillSource(db, companyId, row.id); } + return set.where(condition); + } }; + } }; + }; + } }))); + } }); + await admitDeferredSkillImports(database, companyId, new Date()); + const [saved] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, binding!.id)); + expect(detached).toBe(true); + expect(saved!.active).toBe(false); + expect(saved!.lastError).toBeNull(); + }); + + it('bounds the company pending queue across projects without losing deferred provenance', async () => { + const projectsToInsert = Array.from({ length: MAX_COMPANY_PENDING_SKILL_IMPORTS + 1 }, (_, index) => ({ id: randomUUID(), companyId, name: `Quota project ${index}` })); + await db.insert(projects).values(projectsToInsert); + const workspaces = await db.insert(projectWorkspaces).values(projectsToInsert.map((project, index) => ({ companyId, projectId: project.id, + name: 'Repository', repoUrl: 'https://github.com/acme/skills', defaultRef: `company-quota-${index}` }))).returning(); + for (const row of workspaces) await enqueueProjectSkillSource(db, row); + const pending = await db.select().from(projectSkillSources).where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.active, true))); + expect(pending.filter(row => !row.sourceId)).toHaveLength(MAX_COMPANY_PENDING_SKILL_IMPORTS); + const deferred = await db.select().from(projectSkillSources).where(eq(projectSkillSources.lastError, SKILL_IMPORT_CAPACITY_WAIT)); + expect(deferred).toHaveLength(1); + expect(deferred[0]!.workspaceId).toBeTruthy(); + await db.update(projectSkillSources).set({ active: false, lastError: null }); + }); + + it('admits another project beyond a full project’s oldest waiting batch', async () => { + const full = await workspace('full-admission-project'); + const other = await workspace('other-admission-project'); + const rows = await db.insert(projectWorkspaces).values(Array.from({ length: 120 }, (_, index) => ({ companyId, projectId: full.projectId, + name: `Queued ${index}`, repoUrl: full.repoUrl, defaultRef: `starvation-${index}` }))).returning(); + await db.insert(projectSkillSources).values(rows.map((row, index) => ({ companyId, projectId: row.projectId, workspaceId: row.id, + repositoryUrl: row.repoUrl!, trackingRef: row.defaultRef!, active: index < 20, + lastError: index < 20 ? null : SKILL_IMPORT_CAPACITY_WAIT, createdAt: new Date(0) }))); + const [waiting] = await db.insert(projectSkillSources).values({ companyId, projectId: other.projectId, workspaceId: other.id, + repositoryUrl: other.repoUrl!, trackingRef: other.defaultRef!, active: false, lastError: SKILL_IMPORT_CAPACITY_WAIT }).returning(); + await admitDeferredSkillImports(db, companyId, new Date()); + const [saved] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, waiting!.id)); + expect(saved!.active).toBe(true); + await db.update(projectSkillSources).set({ active: false, lastError: null }); + }); + + it('adoption waits for the company queue before taking repository binding locks', async () => { + const row = await workspace('adoption-lock-order'); + const binding = await enqueueProjectSkillSource(db, row); + const [source] = await db.insert(companySkillSources).values({ companyId, repositoryUrl: row.repoUrl!, fullName: 'acme/skills', trackingRef: row.defaultRef! }).returning(); + await db.update(projectSkillSources).set({ sourceId: source!.id }).where(eq(projectSkillSources.id, binding!.id)); + let unlock!: () => void, locked!: () => void; + const release = new Promise(resolve => { unlock = resolve; }); + const ready = new Promise(resolve => { locked = resolve; }); + let editTx!: typeof db; + const edit = db.transaction(async tx => { editTx = tx as unknown as typeof db; await lockSkillImportQueue(editTx, companyId); locked(); await release; }); + await ready; + const adoption = withSkillActor({ type: 'board', source: 'local_implicit', companyIds: [companyId] }, + () => adoptProjectSkillRepository(db, companyId, row.id, context.authorize)); + try { + await vi.waitFor(async () => { + const result = await db.execute(sql`select 1 from pg_stat_activity where wait_event_type = 'Lock' and query ilike '%companies%for no key update%'`); + expect(result.length).toBeGreaterThan(0); + }); + // A repository edit holding the company lock must still be able to lock + // this binding. The old inverse adoption order fails with lock_not_available. + await editTx.execute(sql`select id from project_skill_sources where id = ${binding!.id} for update nowait`); + } finally { unlock(); await edit; await adoption; } + }); + + it('meters failed initial downloads across workers and retries before provider reads', async () => { + const budgetCompany = randomUUID(); + await db.insert(companies).values({ id: budgetCompany, name: 'Initial work credits', issuePrefix: 'IWC' }); + const [project] = await db.insert(projects).values({ companyId: budgetCompany, name: 'Repeated failed downloads' }).returning(); + const rows = await db.insert(projectWorkspaces).values(Array.from({ length: 5 }, (_, index) => ({ companyId: budgetCompany, + projectId: project!.id, name: `Failed ${index}`, repoUrl: 'https://github.com/acme/skills', defaultRef: `failed-${index}` }))).returning(); + for (const row of rows) await enqueueProjectSkillSource(db, row); + const reads = vi.fn(() => { + const reader = githubFixture({}); + reader.openSnapshot = async () => { throw unprocessable('Git download exceeds 128 MiB.'); }; + return reader; + }); + const factory = async (setup: { companyId: string }) => setup.companyId === budgetCompany ? { ...context, read: reads } : context; + const now = new Date(); + try { + await skillSourceSyncService(db, { context: factory }).tick(now); + expect(reads).toHaveBeenCalledTimes(4); + const [budget] = await db.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, budgetCompany)); + expect(budget!.availableBytes).toBe(0); + const worker = skillSourceSyncService(db, { context: factory }); + await worker.tick(new Date(now.getTime() + 5 * 60_000)); + expect(reads).toHaveBeenCalledTimes(4); + const waiting = await db.select().from(projectSkillSources).where(eq(projectSkillSources.companyId, budgetCompany)); + expect(waiting.every(row => row.active && row.status === 'error' && row.lastError?.includes('limited'))).toBe(true); + await worker.tick(new Date(now.getTime() + 15 * 60_000)); + expect(reads).toHaveBeenCalledTimes(5); + } finally { await db.update(companies).set({ status: 'archived' }).where(eq(companies.id, budgetCompany)); } + }); + + it('refunds measured small initial imports while completed jobs keep a company budget', async () => { + const budgetCompany = randomUUID(); + await db.insert(companies).values({ id: budgetCompany, name: 'Small initial imports', issuePrefix: 'SII' }); + const [project] = await db.insert(projects).values({ companyId: budgetCompany, name: 'Small repositories' }).returning(); + const rows = await db.insert(projectWorkspaces).values(Array.from({ length: 6 }, (_, index) => ({ companyId: budgetCompany, + projectId: project!.id, name: `Small ${index}`, repoUrl: 'https://github.com/acme/skills', defaultRef: `small-${index}` }))).returning(); + for (const row of rows) await enqueueProjectSkillSource(db, row); + const opened = vi.fn(); + const reads = vi.fn(() => { + const reader = githubFixture({}), open = reader.openSnapshot!; + reader.openSnapshot = async (...args) => { opened(); return open(...args); }; + return reader; + }); + try { + await skillSourceSyncService(db, { context: async setup => setup.companyId === budgetCompany ? { ...context, read: reads } : context }).tick(); + const bindings = await db.select().from(projectSkillSources).where(eq(projectSkillSources.companyId, budgetCompany)); + expect(bindings.every(row => row.status === 'ready' && row.sourceId)).toBe(true); + expect(opened).toHaveBeenCalledTimes(6); + const [budget] = await db.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, budgetCompany)); + expect(budget!.availableBytes).toBeGreaterThan(500 * 1024 * 1024); + } finally { await db.update(companies).set({ status: 'archived' }).where(eq(companies.id, budgetCompany)); } + }); + + it('does not replenish initial work credits when large measured jobs complete', async () => { + const budgetCompany = randomUUID(); + await db.insert(companies).values({ id: budgetCompany, name: 'Completed work credits', issuePrefix: 'CWC' }); + const [project] = await db.insert(projects).values({ companyId: budgetCompany, name: 'Large measured imports' }).returning(); + const rows = await db.insert(projectWorkspaces).values(Array.from({ length: 5 }, (_, index) => ({ companyId: budgetCompany, + projectId: project!.id, name: `Measured ${index}`, repoUrl: 'https://github.com/acme/skills', defaultRef: `measured-${index}` }))).returning(); + for (const row of rows) await enqueueProjectSkillSource(db, row); + const opened = vi.fn(); + const reads = vi.fn(() => { + const reader = githubFixture({}), open = reader.openSnapshot!; + reader.openSnapshot = async (...args) => { opened(); return { ...await open(...args), downloadBytes: 128 * 1024 * 1024 }; }; + return reader; + }); + try { + await skillSourceSyncService(db, { context: async setup => setup.companyId === budgetCompany ? { ...context, read: reads } : context }).tick(); + const bindings = await db.select().from(projectSkillSources).where(eq(projectSkillSources.companyId, budgetCompany)); + expect(bindings.filter(row => row.status === 'ready' && row.sourceId)).toHaveLength(4); + expect(bindings.filter(row => row.status === 'error' && row.lastError?.includes('limited'))).toHaveLength(1); + expect(opened).toHaveBeenCalledTimes(4); + const [budget] = await db.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, budgetCompany)); + expect(budget!.availableBytes).toBe(0); + } finally { await db.update(companies).set({ status: 'archived' }).where(eq(companies.id, budgetCompany)); } + }); + + it('never counts refill time twice when an older sweep reserves after a newer one', async () => { + const row = await workspace('monotonic-initial-work'); + const binding = await enqueueProjectSkillSource(db, row); + const token = randomUUID(); + await db.update(projectSkillSources).set({ leaseToken: token }).where(eq(projectSkillSources.id, binding!.id)); + await db.delete(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, companyId)); + const older = new Date(), newer = new Date(older.getTime() + 15 * 60_000); + try { + await reserveInitialSkillWork(db, binding!, token, newer); + await reserveInitialSkillWork(db, binding!, token, older); + const [afterOlder] = await db.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, companyId)); + expect(afterOlder!.updatedAt).toEqual(newer); + await reserveInitialSkillWork(db, binding!, token, newer); + const [saved] = await db.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, companyId)); + expect(saved!.availableBytes).toBe(128 * 1024 * 1024); + } finally { + await detachProjectSkillSource(db, companyId, row.id); + await db.delete(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, companyId)); + } + }); + + it('does not adopt a stale repository after an editor commits a new selection', async () => { + const row = await workspace('adoption-stale-selection'); + await enqueueProjectSkillSource(db, row); + try { + await withSkillActor({ type: 'board', source: 'local_implicit', companyIds: [companyId] }, async () => { + await expect(adoptProjectSkillRepository(db, companyId, row.id, async () => { + await projectService(db).updateWorkspace(row.projectId, row.id, { repoUrl: 'https://github.com/acme/skills-next', defaultRef: 'edited-branch' }); + })).rejects.toThrow(/repository changed/i); + }); + const saved = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + expect(saved.find(binding => binding.trackingRef === 'edited-branch')!.active).toBe(true); + expect(saved.find(binding => binding.trackingRef === row.defaultRef)!.active).toBe(false); + } finally { await detachProjectSkillSource(db, companyId, row.id); } + }); + + it('does not read GitHub or retry an initial job denied by import policy', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + const row = await workspace('denied-preflight'); + await enqueueProjectSkillSource(db, row); + const read = vi.fn(context.read); + const worker = skillSourceSyncService(db, { context: async () => ({ ...context, read, + authorize: async () => { throw forbidden('Skill import denied by policy.'); } }) }); + expect((await worker.tick()).imported).toBe(0); + expect(read).not.toHaveBeenCalled(); + const [blocked] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + expect(blocked).toMatchObject({ active: false, status: 'error', sourceId: null }); + await worker.tick(new Date(Date.now() + 60 * 60_000)); + expect(read).not.toHaveBeenCalled(); + }); + + it('does not add readers when an import overlaps private source removal', async () => { + files = { 'race/SKILL.md': md('race workflow') }; + const originalWorkspace = await workspace('removal-race', 'private'); + await enqueueProjectSkillSource(db, originalWorkspace); + await skillSourceSyncService(db, { context: async () => context }).tick(); + const [origin] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, originalWorkspace.id)); + const sources = skillSourceService(db), skills = companySkillService(db); + const source = await sources.detail(companyId, origin!.sourceId!); + const skillId = source.entries[0]!.skillId!; + const pendingWorkspace = await workspace('removal-race', 'open'); + await enqueueProjectSkillSource(db, pendingWorkspace); + let removed = false, authorizations = 0; + const racingContext = { ...context, authorize: async () => { + if (++authorizations !== 2 || removed) return; + removed = true; + await sources.disconnect(companyId, source.id, context, { revision: source.revision, skills: 'copy' }); + await skills.updateFile(companyId, skillId, 'SKILL.md', md('Restricted race edits')); + } }; + const worker = skillSourceSyncService(db, { context: async () => racingContext }); + expect((await worker.tick()).imported).toBe(0); + const [pending] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, pendingWorkspace.id)); + expect(pending).toMatchObject({ sourceId: null, status: 'error' }); + const readerId = randomUUID(); + await db.insert(authUsers).values({ id: readerId, name: 'Open reader', email: `${readerId}@example.test`, createdAt: new Date(), updatedAt: new Date() }); + await db.insert(companyMemberships).values({ companyId, principalType: 'user', principalId: readerId, membershipRole: 'member', status: 'active' }); + await withSkillActor(actor(readerId), async () => { + expect(await skills.getById(companyId, skillId)).toBeNull(); + expect(await skills.readFile(companyId, skillId, 'SKILL.md')).toBeNull(); + }); + expect((await worker.tick(new Date(Date.now() + 6 * 60_000))).imported).toBe(1); + const [retried] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, pendingWorkspace.id)); + expect(retried!.sourceId).not.toBe(source.id); + }); + + it('imports after durable enqueue, imports later additions and avoids unchanged downloads', async () => { + files = { 'one/SKILL.md': md('one') }; commit = 'a'.repeat(40); downloads.mockClear(); + const row = await workspace('automatic'); + const binding = await enqueueProjectSkillSource(db, row); + await enqueueProjectSkillSource(db, row); + expect(await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id))).toHaveLength(1); + expect((await db.select().from(companySkillSources)).filter(source => source.trackingRef === 'automatic')).toHaveLength(0); + const worker = skillSourceSyncService(db, { context: async () => context }); + expect((await worker.tick()).imported).toBe(1); + const [ready] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, binding!.id)); + const sourceId = ready!.sourceId!; + const first = await skillSourceService(db).detail(companyId, sourceId); + expect(first.accessScope).toBe('projects'); + expect(first.importNewSkills).toBe(true); + const installedId = first.entries[0]!.skillId!; + const original = (await companySkillService(db).getById(companyId, installedId))!; + expect(original.accessSourceId).toBe(sourceId); + const downloadCount = downloads.mock.calls.length; + await worker.tick(new Date(Date.now() + 6 * 60_000)); + expect(downloads).toHaveBeenCalledTimes(downloadCount); + files['one/SKILL.md'] = md('one improved'); files['two/SKILL.md'] = md('two'); commit = 'b'.repeat(40); + await worker.tick(new Date(Date.now() + 6 * 60_000)); + const updated = await skillSourceService(db).detail(companyId, sourceId); + expect(updated.entries.filter(entry => entry.skillId)).toHaveLength(2); + const latest = (await companySkillService(db).getById(companyId, installedId))!; + expect(latest.id).toBe(original.id); + expect(latest.currentVersionId).not.toBe(original.currentVersionId); + const pinned = await companySkillService(db).getVersion(companyId, installedId, original.currentVersionId!); + expect(pinned!.fileInventory.find(file => file.path === 'SKILL.md')!.content).toBe(md('one')); + await detachProjectSkillSource(db, companyId, row.id); + const before = downloads.mock.calls.length; + await worker.tick(new Date(Date.now() + 6 * 60_000)); + expect(downloads).toHaveBeenCalledTimes(before); + expect(await companySkillService(db).getById(companyId, installedId)).not.toBeNull(); + expect(await skillSourceService(db).detail(companyId, sourceId)).toMatchObject({ trackingActive: false, syncState: 'paused' }); + }); + + it('watches empty repositories, retains folder exclusions and coalesces worker sweeps', async () => { + files = {}; commit = 'c'.repeat(40); + const row = await workspace('empty'); + await enqueueProjectSkillSource(db, row); + const worker = skillSourceSyncService(db, { context: async () => context }); + const first = worker.tick(); + expect(worker.tick()).toBe(first); + await first; + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + expect(binding!.status).toBe('ready'); + const service = skillSourceService(db); + const source = await service.detail(companyId, binding!.sourceId!); + await service.refresh(companyId, source.id, context, { revision: source.revision, selectedPaths: [], excludedFolders: ['excluded'] }); + files = { 'excluded/secret/SKILL.md': md('excluded'), 'new/SKILL.md': md('new') }; commit = 'd'.repeat(40); + await worker.tick(new Date(Date.now() + 6 * 60_000)); + const result = await service.detail(companyId, source.id); + expect(result.entries.find(entry => entry.path === 'new/SKILL.md')!.skillId).toBeTruthy(); + expect(result.entries.find(entry => entry.path.includes('secret'))!.selection).toBe('excluded'); + }); + + it('inherits either project access, rechecks revocation and fails closed after project deletion', async () => { + files = { 'private/SKILL.md': md('private workflow') }; commit = 'e'.repeat(40); + const row = await workspace('private', 'private'); + await enqueueProjectSkillSource(db, row); + await skillSourceSyncService(db, { context: async () => context }).tick(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + expect(binding!.status, binding!.lastError ?? 'initial private binding').toBe('ready'); + const source = await skillSourceService(db).detail(companyId, binding!.sourceId!); + const skillId = source.entries[0]!.skillId!; + let privateSnapshotPath = ''; + const privateDerivatives: string[] = []; + for (const user of ['alice', 'bob']) { + await db.insert(authUsers).values({ id: user, name: user, email: `${user}@example.test`, createdAt: new Date(), updatedAt: new Date() }); + await db.insert(companyMemberships).values({ companyId, principalType: 'user', principalId: user, membershipRole: 'member', status: 'active' }); + } + await db.insert(projectAccessMembers).values({ companyId, projectId: row.projectId, subjectType: 'user', subjectId: 'alice' }); + await withSkillActor(actor('alice'), async () => { + expect(await companySkillService(db).getById(companyId, skillId)).not.toBeNull(); + expect((await companySkillService(db).list(companyId)).some(skill => skill.id === skillId)).toBe(true); + const skill = (await companySkillService(db).getById(companyId, skillId))!; + const [runtime] = await companySkillService(db).listRuntimeSkillEntries(companyId, { onlyKeys: new Set([skill.key]) }); + privateSnapshotPath = runtime!.source; + await expect(companySkillService(db).importFromSource(companyId, runtime!.source)).rejects.toThrow(/Project-derived skill/); + const fork = await companySkillService(db).forkSkill(companyId, skillId, { slug: 'private-copy' }); + expect(fork.skill.accessSourceId).not.toBe(source.id); + expect(fork.skill.accessSourceId).toBeTruthy(); + privateDerivatives.push(fork.skill.id); + await expect(companySkillService(db).updateSkill(companyId, fork.skill.id, { sharingScope: 'public_link' })).rejects.toThrow(/public link/); + const copy = await companySkillService(db).createLocalSkill(companyId, { name: 'Private derived copy', forkedFromSkillId: skillId }); + expect(copy.accessSourceId).not.toBe(source.id); + expect(copy.accessSourceId).toBeTruthy(); + privateDerivatives.push(copy.id); + await expect(companySkillService(db).createLocalSkill(companyId, { name: 'Unsafe publication', forkedFromSkillId: skillId, sharingScope: 'public_link' })).rejects.toThrow(/public link/); + }); + await withSkillActor(actor('bob'), async () => { + await expect(companySkillService(db).importFromSource(companyId, privateSnapshotPath)).rejects.toThrow(/not found/); + expect(await companySkillService(db).getById(companyId, skillId)).toBeNull(); + expect(await companySkillService(db).readFile(companyId, skillId, 'SKILL.md')).toBeNull(); + expect((await skillSourceService(db).list(companyId)).some(item => item.id === source.id)).toBe(false); + await expect(skillAccessService(db).assertSkill(companyId, skillId)).rejects.toThrow(/not found/); + }); + const [reader] = await db.insert(agents).values({ companyId, name: 'Private skill reader', adapterType: 'codex_local', status: 'idle' }).returning(); + const [outsider] = await db.insert(agents).values({ companyId, name: 'Unassigned reader', adapterType: 'codex_local', status: 'idle' }).returning(); + for (const agent of [reader!, outsider!]) await db.insert(companyMemberships).values({ companyId, principalType: 'agent', principalId: agent.id, membershipRole: 'member', status: 'active' }); + await db.insert(projectAccessMembers).values({ companyId, projectId: row.projectId, subjectType: 'agent', subjectId: reader!.id }); + const agentActor = (agentId: string, userId: string): AuthorizationActor => ({ + type: 'agent', source: 'agent_jwt', companyId, agentId, runId: randomUUID(), onBehalfOfUserId: userId, + }); + await withSkillActor(agentActor(reader!.id, 'alice'), async () => { + expect(await companySkillService(db).readFile(companyId, skillId, 'SKILL.md')).not.toBeNull(); + const skill = (await companySkillService(db).getById(companyId, skillId))!; + expect(await companySkillService(db).getVersion(companyId, skillId, skill.currentVersionId!)).not.toBeNull(); + expect((await companySkillService(db).listRuntimeSkillEntries(companyId)).some(entry => entry.key === skill.key)).toBe(true); + }); + for (const denied of [agentActor(reader!.id, 'bob'), agentActor(outsider!.id, 'alice'), { ...agentActor(reader!.id, 'alice'), companyId: randomUUID() }]) { + await withSkillActor(denied, async () => { + expect(await companySkillService(db).readFile(companyId, skillId, 'SKILL.md')).toBeNull(); + expect((await companySkillService(db).listRuntimeSkillEntries(companyId)).some(entry => entry.key.includes('private'))).toBe(false); + }); + } + const second = await workspace('private', 'private'); + await db.insert(projectAccessMembers).values({ companyId, projectId: second.projectId, subjectType: 'user', subjectId: 'bob' }); + await enqueueProjectSkillSource(db, second); + await skillSourceSyncService(db, { context: async () => context }).tick(); + const [linked] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, second.id)); + expect(linked!.status, linked!.lastError ?? 'second project binding').toBe('ready'); + expect(linked!.sourceId).toBe(source.id); + await withSkillActor(actor('bob'), async () => expect(await companySkillService(db).getById(companyId, skillId)).not.toBeNull()); + await db.delete(projectAccessMembers).where(and(eq(projectAccessMembers.projectId, second.projectId), eq(projectAccessMembers.subjectId, 'bob'))); + await withSkillActor(actor('bob'), async () => expect(await companySkillService(db).getById(companyId, skillId)).toBeNull()); + const opened = await workspace('private', 'open'); + await enqueueProjectSkillSource(db, opened); + await skillSourceSyncService(db, { context: async () => context }).tick(); + await withSkillActor(actor('bob'), async () => { + expect(await companySkillService(db).getById(companyId, skillId)).not.toBeNull(); + for (const derivativeId of privateDerivatives) { + expect(await companySkillService(db).getById(companyId, derivativeId)).toBeNull(); + expect(await companySkillService(db).readFile(companyId, derivativeId, 'SKILL.md')).toBeNull(); + } + }); + await withSkillActor(agentActor(reader!.id, 'bob'), async () => { + expect(await companySkillService(db).getById(companyId, skillId)).not.toBeNull(); + for (const derivativeId of privateDerivatives) expect(await companySkillService(db).getById(companyId, derivativeId)).toBeNull(); + }); + await db.delete(projects).where(eq(projects.id, row.projectId)); + await db.delete(projects).where(eq(projects.id, second.projectId)); + await withSkillActor(actor('alice'), async () => { + for (const id of privateDerivatives) expect(await companySkillService(db).getById(companyId, id)).toBeNull(); + }); + await db.delete(projects).where(eq(projects.id, opened.projectId)); + await withSkillActor(actor('alice'), async () => expect(await companySkillService(db).getById(companyId, skillId)).toBeNull()); + expect((await db.select().from(companySkills).where(eq(companySkills.id, skillId)))[0]!.accessSourceId).toBe(source.id); + }); + + it.each(['copy', 'delete'] as const)('removes project tracking with %s while retaining dynamic copy/fork access', async choice => { + files = { 'private/SKILL.md': md('retained private workflow') }; commit = '9'.repeat(40); + const row = await workspace(`removed-${choice}`, 'private'); + await enqueueProjectSkillSource(db, row); + const worker = skillSourceSyncService(db, { context: async () => context }); + await worker.tick(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + const service = skillSourceService(db), skills = companySkillService(db); + const source = await service.detail(companyId, binding!.sourceId!); + const original = (await skills.getById(companyId, source.entries[0]!.skillId!))!; + const userId = randomUUID(); + await db.insert(authUsers).values({ id: userId, name: 'Source owner', email: `${userId}@example.test`, createdAt: new Date(), updatedAt: new Date() }); + await db.insert(companyMemberships).values({ companyId, principalType: 'user', principalId: userId, membershipRole: 'member', status: 'active' }); + await db.insert(projectAccessMembers).values({ companyId, projectId: row.projectId, subjectType: 'user', subjectId: userId }); + await withSkillActor(actor(userId), async () => { + const fork = await skills.forkSkill(companyId, original.id, { slug: `retained-${choice}-fork` }); + await skills.updateFile(companyId, fork.skill.id, 'SKILL.md', md('Private fork edits') + '\nConfidential original-project notes.\n'); + const result = await service.disconnect(companyId, source.id, context, { revision: source.revision, skills: choice }); + expect(result).toMatchObject({ copiedCount: choice === 'copy' ? 1 : 0, deletedCount: choice === 'delete' ? 1 : 0 }); + await expect(service.detail(companyId, source.id)).rejects.toThrow(/not found/); + expect((await service.list(companyId)).some(item => item.id === source.id)).toBe(false); + expect((await projectSkillRepositories(db, companyId, actor(userId))).find(item => item.workspaceId === row.id)).toMatchObject({ sourceId: null, status: 'available' }); + expect(await skills.getById(companyId, fork.skill.id)).not.toBeNull(); + let retainedVersionId = original.currentVersionId; + if (choice === 'copy') { + expect(await skills.getById(companyId, original.id)).toMatchObject({ sourceType: 'local_path', accessSourceId: source.id }); + expect((await skills.readFile(companyId, original.id, '.paperclip-repository/private/SKILL.md'))?.content).toBe(md('retained private workflow')); + expect((await skills.readFile(companyId, original.id, 'SKILL.md'))?.content).toContain('.paperclip-repository/private/SKILL.md'); + await skills.updateFile(companyId, original.id, 'SKILL.md', md('Private copy edits') + '\nConfidential original-project notes.\n'); + retainedVersionId = (await skills.createVersion(companyId, original.id, { label: 'Private edited copy' })).id; + } else expect(await skills.getById(companyId, original.id)).toBeNull(); + await db.update(companySkillSources).set({ automaticUpdates: false }).where(eq(companySkillSources.companyId, companyId)); + const before = downloads.mock.calls.length; + await worker.tick(new Date(Date.now() + 6 * 60_000)); + expect(downloads).toHaveBeenCalledTimes(before); + // Reattachment retains disconnection; adoption is the deliberate restart action. + await enqueueProjectSkillSource(db, row); + await worker.tick(new Date(Date.now() + 6 * 60_000)); + expect(downloads).toHaveBeenCalledTimes(before); + const legacy = await workspace(`removed-${choice}`, 'open'); + await db.insert(projectAccessMembers).values({ companyId, projectId: legacy.projectId, subjectType: 'user', subjectId: userId }); + // This older workspace has no binding/source ID; one explicit adoption must work. + await adoptProjectSkillRepository(db, companyId, legacy.id, context.authorize); + await worker.tick(new Date(Date.now() + 6 * 60_000)); + await expect(service.detail(companyId, source.id)).rejects.toThrow(/not found/); + const [adopted] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, legacy.id)); + expect(adopted!.status, adopted!.lastError ?? 'adopted binding').toBe('ready'); + expect(adopted!.sourceId).toBeTruthy(); + expect(adopted!.sourceId).not.toBe(source.id); + const restored = await service.detail(companyId, adopted!.sourceId!); + expect(restored.enabled).toBe(true); + expect(restored.lastError).toBeNull(); + expect(restored.entries.filter(entry => entry.skillId)).toHaveLength(1); + expect(restored.entries[0]!.skillId).not.toBe(original.id); + if (choice === 'copy') expect(await skills.getById(companyId, original.id)).toMatchObject({ key: original.key, sourceType: 'local_path', currentVersionId: retainedVersionId }); + const readerId = randomUUID(); + await db.insert(authUsers).values({ id: readerId, name: 'Open project reader', email: `${readerId}@example.test`, createdAt: new Date(), updatedAt: new Date() }); + await db.insert(companyMemberships).values({ companyId, principalType: 'user', principalId: readerId, membershipRole: 'member', status: 'active' }); + await withSkillActor(actor(readerId), async () => { + const upstream = restored.entries[0]!.skillId!; + expect(await skills.getById(companyId, upstream)).not.toBeNull(); + expect(await skills.getById(companyId, fork.skill.id)).toBeNull(); + expect(await skills.readFile(companyId, fork.skill.id, 'SKILL.md')).toBeNull(); + if (choice === 'copy') { + expect(await skills.getById(companyId, original.id)).toBeNull(); + expect(await skills.readFile(companyId, original.id, 'SKILL.md')).toBeNull(); + expect(await skills.getVersion(companyId, original.id, retainedVersionId!)).toBeNull(); + } + expect((await skills.readFile(companyId, upstream, '.paperclip-repository/private/SKILL.md'))?.content).not.toContain('Confidential'); + // A guessed hidden slug cannot replace or erase someone else's private fork. + const publicFork = await skills.forkSkill(companyId, upstream, { slug: `retained-${choice}-fork` }); + expect(publicFork.skill.id).not.toBe(fork.skill.id); + expect(publicFork.skill.slug).not.toBe(fork.skill.slug); + }); + expect((await skills.readFile(companyId, fork.skill.id, 'SKILL.md'))?.content).toContain('Confidential original-project notes'); + // Explicit adoption of the original workspace also moves tracking, not provenance. + await adoptProjectSkillRepository(db, companyId, row.id, context.authorize); + await worker.tick(new Date(Date.now() + 6 * 60_000)); + const [rebound] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + expect(rebound!.sourceId).toBe(restored.id); + expect(await skills.getById(companyId, fork.skill.id)).not.toBeNull(); + await db.delete(projects).where(eq(projects.id, legacy.projectId)); + await db.delete(projectAccessMembers).where(and(eq(projectAccessMembers.projectId, row.projectId), eq(projectAccessMembers.subjectId, userId))); + expect(await skills.getById(companyId, fork.skill.id)).toBeNull(); + if (choice === 'copy') expect(await skills.getById(companyId, original.id)).toBeNull(); + await db.delete(projects).where(eq(projects.id, row.projectId)); + expect(await skills.getById(companyId, fork.skill.id)).toBeNull(); + }); + }); + + it('recovers expired jobs, competes through durable leases and respects failed-refresh backoff', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'leased/SKILL.md': md('leased') }; commit = 'f'.repeat(40); + const row = await workspace('leased'); + const binding = await enqueueProjectSkillSource(db, row); + await db.update(projectSkillSources).set({ leaseToken: randomUUID(), leaseExpiresAt: new Date(Date.now() - 1_000) }) + .where(eq(projectSkillSources.id, binding!.id)); + let release!: () => void, scanned!: () => void; + const gate = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { scanned = resolve; }); + const blocking: SkillSourceContext = { ...context, read: () => { + const fixture = context.read(null), open = fixture.openSnapshot!; + fixture.openSnapshot = async (...args) => { scanned(); await gate; return open(...args); }; + return fixture; + } }; + const first = skillSourceSyncService(db, { context: async () => blocking }).tick(); + await started; + expect((await skillSourceSyncService(db, { context: async () => context }).tick()).imported).toBe(0); + release(); expect((await first).imported).toBe(1); + const [ready] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, binding!.id)); + const sourceId = ready!.sourceId!; + const service = skillSourceService(db); + const original = await service.detail(companyId, sourceId); + const failure: SkillSourceContext = { ...context, read: () => (async () => { + throw unprocessable('GitHub request limit reached.', { retryAfterMs: 7_200_000 }); + }) as ReturnType }; + const failedAt = new Date(); + await db.update(companySkillSources).set({ nextSyncAt: failedAt }).where(eq(companySkillSources.id, sourceId)); + await skillSourceSyncService(db, { context: async () => failure }).tick(failedAt); + const [failed] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, sourceId)); + expect(failed!.failureCount).toBe(1); + expect(failed!.nextSyncAt.getTime() - failedAt.getTime()).toBeGreaterThanOrEqual(7_200_000); + expect((await service.detail(companyId, sourceId)).entries[0]!.skillId).toBe(original.entries[0]!.skillId); + const retryRead = vi.fn(context.read); + await skillSourceSyncService(db, { context: async () => ({ ...context, read: retryRead }) }) + .tick(new Date(failedAt.getTime() + 6 * 60_000)); + expect(retryRead).not.toHaveBeenCalled(); + await db.update(companySkillSources).set({ automaticUpdates: false, nextSyncAt: new Date() }).where(eq(companySkillSources.id, sourceId)); + await skillSourceSyncService(db, { context: async () => ({ ...context, read: retryRead }) }).tick(); + expect(retryRead).not.toHaveBeenCalled(); + }); + + it('skips live leases before bounding import and refresh batches', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'fair/SKILL.md': md('fair') }; commit = '9'.repeat(40); + const now = new Date(); + const expires = new Date(now.getTime() + 60 * 60_000); + const busyJobs: string[] = []; + for (let index = 0; index < 10; index++) { + const row = await workspace(`busy-import-${index}`); + const binding = await enqueueProjectSkillSource(db, row); + busyJobs.push(binding!.id); + await db.update(projectSkillSources).set({ leaseToken: randomUUID(), leaseExpiresAt: expires, + nextSyncAt: new Date(now.getTime() - (20 - index) * 1_000) }).where(eq(projectSkillSources.id, binding!.id)); + } + const ready = await enqueueProjectSkillSource(db, await workspace('ready-import')); + await db.update(projectSkillSources).set({ nextSyncAt: now }).where(eq(projectSkillSources.id, ready!.id)); + const worker = skillSourceSyncService(db, { context: async () => context }); + expect((await worker.tick(now)).imported).toBe(1); + const [imported] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, ready!.id)); + expect(imported!.sourceId).toBeTruthy(); + for (const id of busyJobs) { + const [job] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, id)); + expect(job).toMatchObject({ sourceId: null, leaseExpiresAt: expires, status: 'pending' }); + } + + const [template] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, imported!.sourceId!)); + await db.update(companySkillSources).set({ automaticUpdates: false }); + const readySourceId = randomUUID(); + await db.insert(companySkillSources).values(Array.from({ length: 11 }, (_, index) => ({ + ...template!, id: index === 10 ? readySourceId : randomUUID(), trackingRef: `fair-refresh-${index}`, + accessScope: 'company' as const, automaticUpdates: true, nextSyncAt: new Date(now.getTime() - (20 - index) * 1_000), + leaseToken: index === 10 ? null : randomUUID(), leaseExpiresAt: index === 10 ? null : expires, + }))); + expect((await worker.tick(now)).checked).toBe(1); + const [refreshed] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, readySourceId)); + expect(refreshed!.nextSyncAt.getTime()).toBeGreaterThan(now.getTime()); + }); + + it('releases interrupted initial imports so a new worker can recover them', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'recovered/SKILL.md': md('recovered') }; commit = '1'.repeat(40); + const row = await workspace('restart'); + const binding = await enqueueProjectSkillSource(db, row); + let scanned!: () => void; + const started = new Promise(resolve => { scanned = resolve; }); + const blocking: SkillSourceContext = { ...context, read: () => { + const fixture = context.read(null); + fixture.openSnapshot = async (_input, options) => { + scanned(); + await new Promise((_resolve, reject) => { + options!.signal!.addEventListener('abort', () => reject(options!.signal!.reason), { once: true }); + }); + throw new Error('Unreachable'); + }; + return fixture; + } }; + const worker = skillSourceSyncService(db, { context: async () => blocking }); + const sweep = worker.tick(); + await started; + await worker.stop(); + await sweep; + const [interrupted] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.id, binding!.id)); + expect(interrupted).toMatchObject({ status: 'pending', failureCount: 0, sourceId: null, leaseToken: null, lastError: null }); + expect((await skillSourceSyncService(db, { context: async () => context }).tick()).imported).toBe(1); + }); + + it('keeps deleted setup agents attributable and requires explicit connection repair', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'owned/SKILL.md': md('owned') }; commit = '2'.repeat(40); + const row = await workspace('deleted-owner'); + const [owner] = await db.insert(agents).values({ companyId, name: 'Source setup agent', adapterType: 'codex_local', status: 'idle' }).returning(); + await enqueueProjectSkillSource(db, row); + const ownerContext: SkillSourceContext = { ...context, setup: { userId: 'local-board', localTrusted: true, agentId: owner!.id } }; + await skillSourceSyncService(db, { context: async () => ownerContext }).tick(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + const sourceId = binding!.sourceId!; + await db.delete(agents).where(eq(agents.id, owner!.id)); + await db.update(companySkillSources).set({ nextSyncAt: new Date() }).where(eq(companySkillSources.id, sourceId)); + const [before] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, sourceId)); + expect(before!.refreshAgentId).toBe(owner!.id); + await skillSourceSyncService(db).tick(); + const [failed] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, sourceId)); + expect(failed!.lastError).toContain('agent who connected'); + expect(failed!.failureCount).toBe(1); + expect((await skillSourceService(db).detail(companyId, sourceId)).entries[0]!.skillId).toBeTruthy(); + }); + + it('backs off a policy denial before acquiring a source lease', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'policy/SKILL.md': md('policy') }; commit = '3'.repeat(40); + const row = await workspace('policy-denial'); + await enqueueProjectSkillSource(db, row); + await skillSourceSyncService(db, { context: async () => context }).tick(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + const now = new Date(); + await db.update(companySkillSources).set({ nextSyncAt: now }).where(eq(companySkillSources.id, binding!.sourceId!)); + const authorize = vi.fn(async () => { throw unprocessable('Automatic updates are denied by company policy.'); }); + const worker = skillSourceSyncService(db, { context: async () => ({ ...context, authorize }) }); + await worker.tick(now); + const [failed] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, binding!.sourceId!)); + expect(failed).toMatchObject({ failureCount: 1, leaseToken: null, lastError: 'Automatic updates are denied by company policy.' }); + expect(failed!.nextSyncAt.getTime()).toBeGreaterThan(now.getTime()); + await worker.tick(new Date(now.getTime() + 1000)); + expect(authorize).toHaveBeenCalledTimes(1); + }); + + it('uses bounded skill packages when a tracked repository outgrows the repository limit', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = { 'small/SKILL.md': md('small') }; commit = '4'.repeat(40); + const row = await workspace('growing-repository'); + await enqueueProjectSkillSource(db, row); + await skillSourceSyncService(db, { context: async () => context }).tick(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, row.id)); + const service = skillSourceService(db), original = await service.detail(companyId, binding!.sourceId!); + expect(original.packageMode).toBe('repository'); + const skillId = original.entries[0]!.skillId!; + files['small/SKILL.md'] = md('small updated'); commit = '5'.repeat(40); + const growing: SkillSourceContext = { ...context, read: () => { + const fixture = context.read(null), open = fixture.openSnapshot; + fixture.openSnapshot = async (...args) => { + const snapshot = await open(...args); + for (let index = 0; index < 10000; index++) snapshot.entries.push({ path: `unrelated/${index}.txt`, type: 'blob', mode: '100644', sha: String(index + 1), size: 0 }); + return snapshot; + }; + return fixture; + } }; + const result = await service.refresh(companyId, original.id, growing); + expect(result.source.packageMode).toBe('skills'); + expect(result.warnings.join(' ')).toContain('repository exceeds'); + expect(result.source.entries[0]!.skillId).toBe(skillId); + expect(result.updated).toHaveLength(1); + files = { 'small/SKILL.md': md('small with shared guide') + '\nRead [guide](../shared/guide.md).', 'shared/guide.md': 'Now fits in the repository package.' }; + commit = '6'.repeat(40); + const complete = await service.refresh(companyId, original.id, context); + expect(complete.source.packageMode).toBe('repository'); + expect(complete.source.entries.find(entry => entry.path === 'small/SKILL.md')!.skillId).toBe(skillId); + expect((await companySkillService(db).readFile(companyId, skillId, '.paperclip-repository/shared/guide.md'))?.content) + .toBe('Now fits in the repository package.'); + }); + + it('enqueues repository changes atomically, leaves ordinary legacy edits alone and retains old references', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + const legacy = await workspace('lifecycle'); + const service = projectService(db, { captureWorkspaceUpdates: false }); + await service.updateWorkspace(legacy.projectId, legacy.id, { name: 'Renamed older workspace' }); + expect(await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, legacy.id))).toHaveLength(0); + + await expect(db.transaction(async tx => { + await projectService(tx as unknown as typeof db, { captureWorkspaceUpdates: false }).createWorkspace(legacy.projectId, + { repoUrl: 'https://github.com/acme/skills', defaultRef: 'rolled-back' }); + throw new Error('Required project audit failed'); + })).rejects.toThrow('Required project audit failed'); + expect(await db.select().from(projectWorkspaces).where(eq(projectWorkspaces.defaultRef, 'rolled-back'))).toHaveLength(0); + expect(await db.select().from(projectSkillSources).where(eq(projectSkillSources.trackingRef, 'rolled-back'))).toHaveLength(0); + + files = { 'lifecycle/SKILL.md': md('lifecycle') }; commit = '6'.repeat(40); + const created = (await service.createWorkspace(legacy.projectId, + { repoUrl: 'https://github.com/acme/skills', defaultRef: 'lifecycle-created' }))!; + const worker = skillSourceSyncService(db, { context: async () => context }); + await worker.tick(); + const [originalBinding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, created.id)); + const source = await skillSourceService(db).detail(companyId, originalBinding!.sourceId!); + const skillId = source.entries[0]!.skillId!; + const versionId = (await companySkillService(db).getById(companyId, skillId))!.currentVersionId; + + await service.updateWorkspace(legacy.projectId, created.id, { defaultRef: 'lifecycle-next' }); + await worker.tick(); + const bindings = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, created.id)); + expect(bindings).toHaveLength(2); + expect(bindings.find(binding => binding.id === originalBinding!.id)?.active).toBe(false); + const current = bindings.find(binding => binding.active)!; + expect(current.sourceId).not.toBe(source.id); + const retained = (await companySkillService(db).getById(companyId, skillId))!; + expect(retained.currentVersionId).toBe(versionId); + const newSource = await skillSourceService(db).detail(companyId, current.sourceId!); + expect(newSource.entries[0]!.skillId).not.toBe(skillId); + + await service.updateWorkspace(legacy.projectId, created.id, { defaultRef: 'lifecycle-next' }); + expect(await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, created.id))).toHaveLength(2); + await service.removeWorkspace(legacy.projectId, created.id); + expect(await skillSourceService(db).detail(companyId, current.sourceId!)).toMatchObject({ trackingActive: false }); + expect(await companySkillService(db).getById(companyId, skillId)).not.toBeNull(); + }); + + it('lets API-key agents attach repositories without inventing a setup person', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + const project = await workspace('key-agent-parent'); + const [agent] = await db.insert(agents).values({ companyId, name: 'API-key project creator', adapterType: 'codex_local', status: 'idle' }).returning(); + const keyActor: AuthorizationActor = { type: 'agent', source: 'agent_key', agentId: agent!.id, companyId, onBehalfOfUserId: 'unverified-person' }; + const attached = await withSkillActor(keyActor, () => projectService(db, { captureWorkspaceUpdates: false }).createWorkspace(project.projectId, + { repoUrl: 'https://github.com/acme/skills', defaultRef: 'key-agent' })); + expect(attached).not.toBeNull(); + const [binding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, attached!.id)); + expect(binding).toMatchObject({ status: 'pending', refreshUserId: null, refreshAgentId: agent!.id, refreshLocalTrusted: false, sourceId: null }); + await detachProjectSkillSource(db, companyId, attached!.id); + }); + + it('requires connection repair before scheduled refresh of API-key public imports', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + files = {}; + const [agent] = await db.insert(agents).values({ companyId, name: 'API-key source importer', adapterType: 'codex_local', status: 'idle' }).returning(); + const setup = await projectSkillSetupIdentity(db, companyId, { type: 'agent', source: 'agent_key', companyId, + agentId: agent!.id, onBehalfOfUserId: 'local-board' }); + const imported = await skillSourceService(db).create(companyId, { repositoryUrl: 'https://github.com/acme/skills', + trackingRef: 'key-generic-public', commitSha: commit, selectedPaths: [] }, { ...context, + actor: { type: 'agent', agentId: agent!.id }, setup: { userId: setup.refreshUserId, agentId: setup.refreshAgentId, localTrusted: setup.refreshLocalTrusted } }); + const [stored] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, imported.source.id)); + expect(stored).toMatchObject({ refreshUserId: null, refreshAgentId: agent!.id, refreshLocalTrusted: false, connectionId: null }); + await db.insert(activityLog).values({ companyId, actorType: 'agent', actorId: agent!.id, agentId: agent!.id, + responsibleUserId: 'local-board', action: 'company.skill_source_refreshed', entityType: 'company_skill_source', entityId: imported.source.id }); + await skillSourceSyncService(db).tick(new Date(Date.now() + 6 * 60_000)); + const [checked] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, imported.source.id)); + expect(checked!.lastError).toMatch(/Choose a GitHub connection/); + expect(checked!.refreshUserId).toBeNull(); + expect(checked!.lastSuccessAt).toEqual(stored!.lastSuccessAt); + // Migrated sources may lack refreshAgentId; API-key audit responsibility + // still cannot stand in for an authenticated run's credential authority. + await db.update(companySkillSources).set({ refreshAgentId: null }).where(eq(companySkillSources.id, imported.source.id)); + await skillSourceSyncService(db).tick(new Date(Date.now() + 12 * 60_000)); + const [legacy] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, imported.source.id)); + expect(legacy!.refreshUserId).toBeNull(); + expect(legacy!.lastError).toMatch(/Choose a GitHub connection/); + + }); + + it('follows a changed default branch while an explicit branch remains fixed', async () => { + await db.update(companySkillSources).set({ automaticUpdates: false }); + let defaultBranch = 'main'; + const originalFiles = { 'workflow/SKILL.md': md('original default') }, nextFiles = { 'workflow/SKILL.md': md('new default') }; + const moving: SkillSourceContext = { ...context, read: () => { + const reader = (async (url: string) => { + if (url === '/repos/acme/skills') return { id: 42, full_name: 'acme/skills', default_branch: defaultBranch }; + if (url.startsWith('/repos/acme/skills/commits/')) return { sha: decodeURIComponent(url.split('/commits/')[1]!) === 'main' ? '7'.repeat(40) : '8'.repeat(40) }; + throw new Error('Unexpected request'); + }) as ReturnType; + reader.openSnapshot = async (input, options) => githubFixture(input.ref === 'main' ? originalFiles : nextFiles, {}, input.ref === 'main' ? '7'.repeat(40) : '8'.repeat(40)).openSnapshot(input, options); + return reader; + } }; + const implicit = await workspace('HEAD'); + await enqueueProjectSkillSource(db, implicit); + const worker = skillSourceSyncService(db, { context: async () => moving }); + await worker.tick(); + const [defaultBinding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, implicit.id)); + const sourceService = skillSourceService(db); + const original = await sourceService.detail(companyId, defaultBinding!.sourceId!); + expect(original.trackingRef).toBe('HEAD'); + const skillId = original.entries[0]!.skillId!; + const versionId = (await companySkillService(db).getById(companyId, skillId))!.currentVersionId; + + const explicit = await workspace('main'); + await enqueueProjectSkillSource(db, explicit); + await worker.tick(); + const [fixedBinding] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, explicit.id)); + expect(fixedBinding!.sourceId).not.toBe(original.id); + defaultBranch = 'next-default'; + await worker.tick(new Date(Date.now() + 6 * 60_000)); + expect(await sourceService.detail(companyId, original.id)).toMatchObject({ trackingRef: 'HEAD', lastScanCommit: '8'.repeat(40) }); + expect(await sourceService.detail(companyId, fixedBinding!.sourceId!)).toMatchObject({ trackingRef: 'main', lastScanCommit: '7'.repeat(40) }); + const latest = (await companySkillService(db).getById(companyId, skillId))!; + expect(latest.currentVersionId).not.toBe(versionId); + expect(latest.markdown).toBe(md('new default')); + expect((await companySkillService(db).getVersion(companyId, skillId, versionId!))!.fileInventory[0]!.content).toBe(md('original default')); + const repeated = await workspace('HEAD'); + await enqueueProjectSkillSource(db, repeated); + await worker.tick(); + const [reused] = await db.select().from(projectSkillSources).where(eq(projectSkillSources.workspaceId, repeated.id)); + expect(reused!.sourceId).toBe(original.id); + }); +}); diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index fadc781e42f..a78ff2a493b 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -251,6 +251,12 @@ vi.mock("../app.js", () => ({ createApp: createAppMock, })); +// This suite exercises startup coordination; source persistence and authority +// are covered by the database-backed source worker tests. +vi.mock("../services/skill-source-sync.js", () => ({ + skillSourceSyncService: () => ({ tick: vi.fn(async () => ({ imported: 0, checked: 0 })), stop: vi.fn(async () => {}) }), +})); + vi.mock("../services/native-runtime/native-session-executor.js", () => ({ verifyStoppedNativeSessionForReplacement: vi.fn(async () => null), })); diff --git a/server/src/__tests__/skill-source-retry.test.ts b/server/src/__tests__/skill-source-retry.test.ts new file mode 100644 index 00000000000..f4027f536fa --- /dev/null +++ b/server/src/__tests__/skill-source-retry.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { unprocessable } from '../errors.js'; +import { skillSourceRetryAt } from '../services/skill-source-retry.js'; + +describe('skill source retry scheduling', () => { + const now = new Date('2026-10-12T12:00:00Z'); + it('backs off repeated failures and caps ordinary retries at an hour', () => { + expect(skillSourceRetryAt(new Error('failed'), 0, now).getTime() - now.getTime()).toBe(300_000); + expect(skillSourceRetryAt(new Error('failed'), 2, now).getTime() - now.getTime()).toBe(1_200_000); + expect(skillSourceRetryAt(new Error('failed'), 20, now).getTime() - now.getTime()).toBe(3_600_000); + }); + it('honors provider rate limits without accepting unbounded or invalid delays', () => { + const retry = (delay: number) => skillSourceRetryAt(unprocessable('Limited', { retryAfterMs: delay }), 0, now).getTime() - now.getTime(); + expect(retry(7_200_000)).toBe(7_200_000); + expect(retry(100)).toBe(300_000); + expect(retry(Infinity)).toBe(300_000); + expect(retry(10 ** 12)).toBe(86_400_000); + }); +}); diff --git a/server/src/__tests__/skill-sources.test.ts b/server/src/__tests__/skill-sources.test.ts index e0e7f8bd800..f8789d86787 100644 --- a/server/src/__tests__/skill-sources.test.ts +++ b/server/src/__tests__/skill-sources.test.ts @@ -37,6 +37,88 @@ describe.skipIf(!support.supported)('skill source persistence', () => { if (originalHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = originalHome; await testDb?.cleanup(); if (home) await fs.rm(home, { recursive: true, force: true }); }); + it('checks a new commit with an unchanged tree without downloading or replacing versions', async () => { + const originalFiles = files, originalCommit = commit; + try { + const service = skillSourceService(db); + files = { 'tree/SKILL.md': md('tree') }; commit = '3'.repeat(40); + const created = await service.create(companyId, { repositoryUrl: 'https://github.com/acme/skills', trackingRef: 'tree-only', commitSha: commit, selectedPaths: ['tree/SKILL.md'] }, context); + const skillId = created.source.entries[0]!.skillId!; + const previous = (await companySkillService(db).getById(companyId, skillId))!.currentVersionId; + await db.update(companySkillSources).set({ lastScanTreeSha: null }).where(eq(companySkillSources.id, created.source.id)); + const currentReader = context.read(null), currentOpen = vi.spyOn(currentReader, 'openSnapshot'); + await service.refresh(companyId, created.source.id, { ...context, read: () => currentReader }, undefined, undefined, { checkCommit: true }); + expect(currentOpen).not.toHaveBeenCalled(); + const [remembered] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, created.source.id)); + expect(remembered!.lastScanTreeSha).toMatch(/^[a-f0-9]{40}$/); + commit = '4'.repeat(40); + const reader = context.read(null), open = vi.spyOn(reader, 'openSnapshot'); + const result = await service.refresh(companyId, created.source.id, { ...context, read: () => reader }, undefined, undefined, { checkCommit: true }); + expect(open).not.toHaveBeenCalled(); + expect(result.source.lastScanCommit).toBe(commit); + expect((await companySkillService(db).getById(companyId, skillId))!.currentVersionId).toBe(previous); + files['tree/SKILL.md'] = md('tree changed'); commit = '5'.repeat(40); + const changed = context.read(null), changedOpen = vi.spyOn(changed, 'openSnapshot'); + await service.refresh(companyId, created.source.id, { ...context, read: () => changed }, undefined, undefined, { checkCommit: true }); + expect(changedOpen).toHaveBeenCalledTimes(1); + expect((await companySkillService(db).getById(companyId, skillId))!.currentVersionId).not.toBe(previous); + } finally { files = originalFiles; commit = originalCommit; } + }); + + it('persists automatic work limits, refills capacity, and allows explicit refresh', async () => { + const originalFiles = files, originalCommit = commit; + try { + files = { 'budget/SKILL.md': md('budget') }; commit = '6'.repeat(40); + let downloads = 0; + const budgetContext: SkillSourceContext = { ...context, read: () => { + const reader = context.read(null), open = reader.openSnapshot; + reader.openSnapshot = async (...args) => { downloads++; return { ...await open(...args), downloadBytes: 128 * 1024 * 1024 }; }; + return reader; + } }; + const service = skillSourceService(db); + const created = await service.create(companyId, { repositoryUrl: 'https://github.com/acme/skills', trackingRef: 'work-budget', commitSha: commit, selectedPaths: ['budget/SKILL.md'] }, budgetContext); + const start = new Date(); + for (let index = 0; index < 4; index++) { + files['budget/SKILL.md'] = md(`budget revision ${index}`); commit = (7 + index).toString(16).repeat(40); + await service.refresh(companyId, created.source.id, budgetContext, undefined, undefined, { checkCommit: true, automatic: true, now: start }); + } + const skillId = created.source.entries[0]!.skillId!; + const version = (await companySkillService(db).getById(companyId, skillId))!.currentVersionId; + files['budget/SKILL.md'] = md('budget next'); commit = 'b'.repeat(40); + const before = downloads; + await expect(skillSourceService(db).refresh(companyId, created.source.id, budgetContext, undefined, undefined, + { checkCommit: true, automatic: true, now: start })).rejects.toThrow(/temporarily limited/); + expect(downloads).toBe(before); + expect((await companySkillService(db).getById(companyId, skillId))!.currentVersionId).toBe(version); + await service.refresh(companyId, created.source.id, budgetContext, undefined, undefined, { checkCommit: true }); + expect(downloads).toBe(before + 1); + const [budgetRow] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, created.source.id)); + expect(budgetRow!.automaticBudgetAt.getTime()).toBe(start.getTime()); + expect(budgetRow!.automaticBudgetBytes).toBe(0); + files['budget/SKILL.md'] = md('budget refilled'); commit = 'c'.repeat(40); + await skillSourceService(db).refresh(companyId, created.source.id, budgetContext, undefined, undefined, + { checkCommit: true, automatic: true, now: new Date(start.getTime() + 15 * 60_000) }); + expect(downloads).toBe(before + 2); + expect((await companySkillService(db).getById(companyId, skillId))!.currentVersionId).not.toBe(version); + } finally { files = originalFiles; commit = originalCommit; } + }); + + it('refunds unused automatic capacity for small repositories', async () => { + const originalFiles = files, originalCommit = commit; + try { + files = { 'small/SKILL.md': md('small') }; commit = 'd'.repeat(40); + const service = skillSourceService(db); + const created = await service.create(companyId, { repositoryUrl: 'https://github.com/acme/skills', trackingRef: 'small-budget', commitSha: commit, selectedPaths: ['small/SKILL.md'] }, context); + const now = new Date(); + for (let index = 0; index < 6; index++) { + files['small/SKILL.md'] = md(`small revision ${index}`); commit = String(index + 1).repeat(40); + await service.refresh(companyId, created.source.id, context, undefined, undefined, { checkCommit: true, automatic: true, now }); + } + const [stored] = await db.select().from(companySkillSources).where(eq(companySkillSources.id, created.source.id)); + expect(stored!.automaticBudgetBytes).toBeGreaterThan(500 * 1024 * 1024); + } finally { files = originalFiles; commit = originalCommit; } + }); + it('imports duplicate names independently, stores complete snapshots and refreshes without duplicates', async () => { const sources = skillSourceService(db), skills = companySkillService(db); const result = await sources.create(companyId, { repositoryUrl: 'https://github.com/acme/skills', commitSha: sha, selectedPaths: ['deep/one/SKILL.md', 'elsewhere/one/SKILL.md'] }, context); @@ -167,7 +249,12 @@ describe.skipIf(!support.supported)('skill source persistence', () => { { companyId: legacyCompany, key: 'legacy/custom', slug: 'original-slug', name: 'Original name', markdown: md('legacy'), sourceType: 'github', sourceRef: sha, metadata: { hostname: 'github.com', owner: 'acme', repo: 'skills', trackingRef: 'legacy', repoSkillDir: 'legacy' } }, { companyId: legacyCompany, key: 'paperclipai/paperclip/bundled', slug: 'bundled', name: 'Bundled', markdown: md('bundled'), sourceType: 'github', metadata: { hostname: 'github.com', owner: 'acme', repo: 'skills', sourceKind: 'paperclip_bundled' } }, ]).returning(); - const migration = await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8'); + // Production runs 0291 before 0341. This harness uses the latest schema, + // whose repository/ref uniqueness is partial; retain the same backfill + // assertions while specifying that index's inference predicate. + const migration = (await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8')) + .replace('ON CONFLICT (company_id, repository_url, tracking_ref) DO NOTHING', + 'ON CONFLICT (company_id, repository_url, tracking_ref) WHERE removed_at IS NULL DO NOTHING'); await db.execute(sql.raw(migration.slice(migration.indexOf('DO $$', migration.indexOf('-- Adopt only'))))); await db.execute(sql.raw(migration.slice(migration.indexOf('DO $$', migration.indexOf('-- Adopt only'))))); const service = skillSourceService(db); @@ -225,7 +312,12 @@ describe.skipIf(!support.supported)('skill source persistence', () => { { companyId: legacyCompany, key: 'legacy/missing-ref', slug: 'old', name: 'Old', markdown: md('old'), sourceType: 'github', sourceRef: sha, metadata: { owner: 'acme', repo: 'skills', ref: sha, repoSkillDir: 'old' } }, { companyId: legacyCompany, key: 'legacy/pinned', slug: 'pinned', name: 'Pinned', markdown: md('pinned'), sourceType: 'github', sourceRef: sha, metadata: { owner: 'acme', repo: 'skills', ref: sha, trackingRef: sha, repoSkillDir: 'pinned' } }, ]).returning(); - const migration = await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8'); + // Production runs 0291 before 0341. This harness uses the latest schema, + // whose repository/ref uniqueness is partial; retain the same backfill + // assertions while specifying that index's inference predicate. + const migration = (await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8')) + .replace('ON CONFLICT (company_id, repository_url, tracking_ref) DO NOTHING', + 'ON CONFLICT (company_id, repository_url, tracking_ref) WHERE removed_at IS NULL DO NOTHING'); await db.execute(sql.raw(migration.slice(migration.indexOf('DO $$', migration.indexOf('-- Adopt only'))))); const service = skillSourceService(db); const adopted = (await service.sourceForSkill(legacyCompany, legacy!.id))!; @@ -250,7 +342,12 @@ describe.skipIf(!support.supported)('skill source persistence', () => { { companyId: legacyCompany, key: 'legacy/implicit', slug: 'implicit', name: 'Implicit', markdown: md('old'), sourceType: 'github', sourceRef: sha, metadata: { owner: 'acme', repo: 'skills', ref: sha, repoSkillDir: 'one' } }, { companyId: legacyCompany, key: 'legacy/explicit', slug: 'explicit', name: 'Explicit', markdown: md('old'), sourceType: 'github', sourceRef: sha, metadata: { owner: 'acme', repo: 'skills', trackingRef: 'main', repoSkillDir: 'one' } }, ]).returning(); - const migration = await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8'); + // Production runs 0291 before 0341. This harness uses the latest schema, + // whose repository/ref uniqueness is partial; retain the same backfill + // assertions while specifying that index's inference predicate. + const migration = (await fs.readFile(new URL('../../../packages/db/src/migrations/0291_conscious_secret_warriors.sql', import.meta.url), 'utf8')) + .replace('ON CONFLICT (company_id, repository_url, tracking_ref) DO NOTHING', + 'ON CONFLICT (company_id, repository_url, tracking_ref) WHERE removed_at IS NULL DO NOTHING'); await db.execute(sql.raw(migration.slice(migration.indexOf('DO $$', migration.indexOf('-- Adopt only'))))); const service = skillSourceService(db); files = { 'one/SKILL.md': md('updated') }; commit = 'e'.repeat(40); @@ -424,6 +521,52 @@ describe.skipIf(!support.supported)('skill source persistence', () => { await expect(fs.stat(path.join(runtime.source, '.paperclip-repository/shared/tools'))).rejects.toMatchObject({ code: 'ENOENT' }); }); + it.each([false, true])('retains installed repository dependencies after an oversized refresh (scheduled: %s)', async scheduled => { + const temporaryCompany = randomUUID(); + await db.insert(companies).values({ id: temporaryCompany, name: 'Repository fallback', issuePrefix: 'FBK' }); + let repoFiles = { + 'skills/main/SKILL.md': md('main') + '\nRead [shared](../../shared/guide.md) and [helper](../helper/SKILL.md).', + 'skills/helper/SKILL.md': md('helper'), + 'shared/guide.md': 'Working shared instructions', + }; + let head = sha; + const localContext = { ...context, read: () => { + const fixture = githubFixture(repoFiles, {}, head); + const open = fixture.openSnapshot; + fixture.openSnapshot = async (...args) => { + const snapshot = await open(...args); + for (const entry of snapshot.entries) if (entry.path.startsWith('generated/')) entry.size = 1024 * 1024; + return snapshot; + }; + return fixture; + } }; + try { + const service = skillSourceService(db), library = companySkillService(db); + const installed = await service.create(temporaryCompany, { repositoryUrl: 'https://github.com/acme/skills', packageMode: 'repository', + selectedPaths: ['skills/main/SKILL.md', 'skills/helper/SKILL.md'] }, localContext); + const main = installed.imported.find(skill => skill.name === 'main')!; + const versions = await library.listVersions(temporaryCompany, main.id); + head = 'f'.repeat(40); + repoFiles = { ...repoFiles, 'shared/guide.md': 'New shared instructions', + ...Object.fromEntries(Array.from({ length: 62 }, (_, i) => [`generated/${i}.bin`, 'x'])) }; + await expect(service.refresh(temporaryCompany, installed.source.id, localContext, undefined, undefined, { checkCommit: scheduled })) + .rejects.toThrow('Installed repository skills and their dependencies were kept'); + const retained = await service.detail(temporaryCompany, installed.source.id); + expect(retained.packageMode).toBe('repository'); + expect(retained.lastScanCommit).toBe(sha); + expect(retained.lastError).toContain('dependencies were kept'); + expect(await library.listVersions(temporaryCompany, main.id)).toEqual(versions); + expect((await library.readFile(temporaryCompany, main.id, '.paperclip-repository/shared/guide.md'))?.content).toBe('Working shared instructions'); + expect((await library.readFile(temporaryCompany, main.id, '.paperclip-repository/skills/helper/SKILL.md'))?.content).toBe(md('helper')); + repoFiles = { 'skills/main/SKILL.md': repoFiles['skills/main/SKILL.md'], 'skills/helper/SKILL.md': md('helper'), 'shared/guide.md': 'New shared instructions' }; + await service.refresh(temporaryCompany, installed.source.id, localContext); + expect((await service.detail(temporaryCompany, installed.source.id)).lastError).toBeNull(); + expect((await library.readFile(temporaryCompany, main.id, '.paperclip-repository/shared/guide.md'))?.content).toBe('New shared instructions'); + } finally { + await deleteCompany(db, temporaryCompany); + } + }); + it('cascades repository snapshots and referencing versions when the owning company is deleted', async () => { const temporaryCompany = randomUUID(); await db.insert(companies).values({ id: temporaryCompany, name: 'Temporary repository company', issuePrefix: 'TMP' }); diff --git a/server/src/app.ts b/server/src/app.ts index 270c368602c..bd8685fe49d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -52,6 +52,7 @@ import { import type { InspectDatabaseBackupHealthOptions } from "./services/database-backup-health.js"; import type { StorageService } from "./storage/types.js"; import { httpLogger, errorHandler } from "./middleware/index.js"; +import { skillAccessRequestContext } from "./services/skill-access.js"; import { actorMiddleware } from "./middleware/auth.js"; import { boardMutationGuard } from "./middleware/board-mutation-guard.js"; import { @@ -655,6 +656,7 @@ export async function createApp( // REPLACES whatever actor the request otherwise resolved to, and only on // the one endpoint it authorizes (see the middleware for the contract). app.use(cloudControlMiddleware()); + app.use(skillAccessRequestContext); app.use("/api/auth", authRoutes(db)); if (opts.betterAuthHandler) { app.all("/api/auth/{*authPath}", opts.betterAuthHandler); diff --git a/server/src/index.ts b/server/src/index.ts index 0f724bc6a24..ab0d225ad95 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,3 +1,4 @@ +import { skillSourceSyncService } from "./services/skill-source-sync.js"; import { subscribeAllCompanyLiveEvents } from "./services/live-events.js"; import { DELIVERY_QUEUES } from "./services/delivery-work-notifications.js"; import type { createDeliveryWorkCoordinator } from "./services/delivery-work-coordinator.js"; @@ -1212,11 +1213,22 @@ async function startServerWithDatabaseTeardown( }, config.heartbeatSchedulerIntervalMs); heartbeatSchedulerInterval?.unref?.(); }; + const skillSourceSync = skillSourceSyncService(db as any); + const sweepSkillSources = () => { + if (isIdleTaskDrainActive() || heartbeatSchedulerStopped || isWarmStandby()) return; + trackHeartbeatSchedulerWork(skillSourceSync.tick().catch(err => { + logger.error({ err }, "skill source synchronization failed"); + })); + }; + const skillSourceSyncInterval = setInterval(sweepSkillSources, 5000); + skillSourceSyncInterval.unref?.(); + sweepSkillSources(); const externalObjects = externalObjectService(db as any, { pluginWorkerManager, enabled: async () => (await instanceSettingsService(db).getExperimental()).enableExternalObjects === true, }); const scheduleExternalObjectRefreshSweep = (now = new Date()) => { + sweepSkillSources(); if (isIdleTaskDrainActive() || heartbeatSchedulerStopped) return; trackHeartbeatSchedulerWork(externalObjects .refreshDueObjectsForActiveCompanies(50, now) @@ -1968,6 +1980,8 @@ async function startServerWithDatabaseTeardown( ) => { await systemdNotify(["--stopping", `--status=Stopping after ${signal}`]); heartbeatSchedulerStopped = true; + clearInterval(skillSourceSyncInterval); + await skillSourceSync.stop(); unsubscribeChatCompletions(); await deliveryWork.stop(); clearInterval(executionControlInterval); diff --git a/server/src/realtime/live-events-ws.ts b/server/src/realtime/live-events-ws.ts index 7fe53906e35..1193e8b6e08 100644 --- a/server/src/realtime/live-events-ws.ts +++ b/server/src/realtime/live-events-ws.ts @@ -1,3 +1,4 @@ +import { skillAccessService } from "../services/skill-access.js"; import { createHash } from "node:crypto"; import type { IncomingMessage, Server as HttpServer } from "node:http"; import { createRequire } from "node:module"; @@ -303,6 +304,13 @@ export function setupLiveEventsWebSocketServer( if (!issue || !(await access.decide({ actor: context!.actor, action: "issue:read", resource: { type: "issue", companyId: event.companyId, issueId } })).allowed) return null; } + if (["company_skill", "company_skill_source"].includes(String(payload.entityType)) && typeof payload.entityId === "string") { + try { + const skills = skillAccessService(db, context!.actor); + if (payload.entityType === "company_skill") await skills.assertSkill(event.companyId, payload.entityId); + else await skills.assertSource(event.companyId, payload.entityId); + } catch { return null; } + } if (payload.entityType === "project" && typeof payload.entityId === "string") { const [project] = await db.select({ id: projects.id }).from(projects) .where(and(eq(projects.id, payload.entityId), eq(projects.companyId, event.companyId))).limit(1); diff --git a/server/src/routes/company-skills.ts b/server/src/routes/company-skills.ts index 97b68b69f8d..a5f78735051 100644 --- a/server/src/routes/company-skills.ts +++ b/server/src/routes/company-skills.ts @@ -1,7 +1,10 @@ +import { projectSkillSetupIdentity } from "../services/project-skill-source-bindings.js"; +import { adoptProjectSkillRepository, projectSkillRepositories, skillSourceSyncService } from "../services/skill-source-sync.js"; +import { skillAccessRequestContext, skillAccessService } from "../services/skill-access.js"; import { skillSourceService, type SkillSourceContext } from "../services/skill-sources.js"; import { skillSourceGitHubReader } from "../services/skill-source-github-access.js"; import { toolAccessService } from "../services/tool-access.js"; -import { skillSourceDisconnectSchema, skillSourceCreateSchema, skillSourceDiscoverySchema, skillSourcePreviewSchema, skillSourceSelectionSchema } from "@paperclipai/shared"; +import { skillSourceCreateSchema, skillSourceDiscoverySchema, skillSourcePreviewSchema, skillSourceSelectionSchema, skillSourceSettingsSchema, projectSkillSourceAdoptSchema, skillSourceDisconnectSchema } from "@paperclipai/shared"; import type { ActivityPublication } from "../services/activity-log.js"; import { once } from "node:events"; import type { SkillSourceDiscoveryEvent } from "@paperclipai/shared"; @@ -52,7 +55,7 @@ import { listCatalogSkillsOrEmpty, readCatalogSkillFile, } from "../services/skills-catalog.js"; -import { badRequest, conflict, forbidden, unauthorized, HttpError } from "../errors.js"; +import { badRequest, conflict, forbidden, unauthorized, notFound, HttpError } from "../errors.js"; import { assertAuthenticated, assertCompanyAccess, getActorInfo } from "./authz.js"; import { getTelemetryClient } from "../telemetry.js"; import { @@ -60,7 +63,7 @@ import { normalizeSkillPolicySourceType, type SkillPolicyPrincipal, } from "../services/company-skill-policy.js"; -import { authorizationDeniedDetails } from "../services/authorization.js"; +import { authorizationDeniedDetails, type AuthorizationActor } from "../services/authorization.js"; import { normalizeSkillPolicySourceLocator, type SkillPolicyAction, @@ -97,9 +100,23 @@ type SkillPolicyResourceInput = export function companySkillRoutes(db: Db) { const router = Router(); + router.use(skillAccessRequestContext); + router.param("skillId", async (req, _res, next, id) => { + try { + if (req.actor.type === "none") throw unauthorized("Authentication required"); + assertCompanyAccess(req, String(req.params.companyId)); + if (!await svc.getById(String(req.params.companyId), id)) throw notFound("Skill not found"); + next(); + } catch (error) { next(error); } + }); + router.param("sourceId", async (req, _res, next, id) => { + try { assertCompanyAccess(req, String(req.params.companyId)); await skillAccessService(db).assertSource(String(req.params.companyId), id); next(); } + catch (error) { next(error); } + }); const access = accessService(db); const svc = companySkillService(db); const sourceSvc = skillSourceService(db); + const sourceSync = skillSourceSyncService(db); const issues = issueService(db); const heartbeat = heartbeatService(db); const skillPolicies = companySkillPolicyService(db); @@ -300,8 +317,10 @@ export function companySkillRoutes(db: Db) { assertCompanyAccess(req, companyId); const actor = getActorInfo(req); const publications: ActivityPublication[] = []; + const setup = await projectSkillSetupIdentity(db, companyId, req.actor); const result = await operation({ actor: skillActor(req), + setup: { userId: setup.refreshUserId, agentId: setup.refreshAgentId, localTrusted: setup.refreshLocalTrusted }, read: connectionId => skillSourceGitHubReader(db, companyId, req.actor, connectionId), authorize: (action, resource) => assertCanMutateCompanySkills(req, companyId, action, resource), audit: async (tx, sourceId, action, details) => { @@ -313,6 +332,21 @@ export function companySkillRoutes(db: Db) { return result; } + router.get("/companies/:companyId/skill-sources/project-repositories", async (req, res) => { + const companyId = String(req.params.companyId); + assertCompanyAccess(req, companyId); + res.json(await projectSkillRepositories(db, companyId, req.actor)); + }); + router.post("/companies/:companyId/skill-sources/adopt-project", validate(projectSkillSourceAdoptSchema), async (req, res) => { + const companyId = String(req.params.companyId); + const result = await sourceOperation(req, companyId, context => adoptProjectSkillRepository(db, companyId, req.body.workspaceId, context.authorize)); + publishActivity(result.publication); + res.status(202).json({ bindingId: result.bindingId }); + }); + router.patch("/companies/:companyId/skill-sources/:sourceId/settings", validate(skillSourceSettingsSchema), async (req, res) => { + const companyId = String(req.params.companyId); + res.json(await sourceOperation(req, companyId, context => sourceSvc.settings(companyId, String(req.params.sourceId), req.body, context))); + }); router.get("/companies/:companyId/skill-sources/repositories", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); @@ -388,7 +422,7 @@ export function companySkillRoutes(db: Db) { }); router.post("/companies/:companyId/skill-sources/:sourceId/refresh", async (req, res) => { const companyId = req.params.companyId as string; - res.json(await sourceOperation(req, companyId, context => sourceSvc.refresh(companyId, req.params.sourceId as string, context))); + res.json(await sourceOperation(req, companyId, context => sourceSync.refreshSource(companyId, req.params.sourceId as string, context))); }); router.delete("/companies/:companyId/skill-sources/:sourceId", (req, _res, next) => { assertCompanyAccess(req, req.params.companyId as string); @@ -702,10 +736,22 @@ export function companySkillRoutes(db: Db) { assigneeAgentId: req.body.agentId, }); const actor = getActorInfo(req); + const skill = await svc.getById(companyId, skillId); + let skillProjectId: string | undefined; + if (skill?.accessSourceId) { + const bindings = await skillAccessService(db).projectIds(companyId, skill.accessSourceId); + const testActor: AuthorizationActor = { type: "agent", agentId: req.body.agentId, companyId, source: "agent_key", + onBehalfOfUserId: req.actor.source === "local_implicit" ? null : req.actor.userId ?? req.actor.onBehalfOfUserId }; + for (const binding of bindings) { + if ((await access.decide({ actor: testActor, action: "project:read", resource: { type: "project", companyId, projectId: binding.projectId } })).allowed) { skillProjectId = binding.projectId; break; } + } + if (!skillProjectId) throw forbidden("Choose an agent with access to this skill's project."); + } const result = await svc.createTestRun(companyId, skillId, req.body, skillActor(req), { createHarnessIssue: async (harnessIssue) => { const created = await issues.create(companyId, { ...harnessIssue, + ...(skillProjectId ? { projectId: skillProjectId, visibility: "private" as const } : {}), priority: "medium", createdByAgentId: actor.agentId, createdByUserId: actor.actorType === "user" ? actor.actorId : null, diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 6ad7f19d721..8e4a71c4bfc 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -172,6 +172,8 @@ import { skillSourcePreviewSchema, skillSourceCreateSchema, skillSourceSelectionSchema, + skillSourceSettingsSchema, + projectSkillSourceAdoptSchema, companySkillCreateSchema, companySkillFileDeleteSchema, companySkillFileUpdateSchema, @@ -12377,6 +12379,9 @@ registerCurrentRoute({ for (const [method, path, summary] of [ ["get", "/api/companies/{companyId}/skill-sources", "List GitHub skill sources"], + ["get", "/api/companies/{companyId}/skill-sources/project-repositories", "List accessible project repositories and skill import status"], + ["post", "/api/companies/{companyId}/skill-sources/adopt-project", "Import all skills from an accessible project repository"], + ["patch", "/api/companies/{companyId}/skill-sources/{sourceId}/settings", "Set automatic skill updates and authorized refresh connection"], ["get", "/api/companies/{companyId}/skill-sources/repositories", "Browse authorized GitHub repositories for skills"], ["get", "/api/companies/{companyId}/skill-sources/{sourceId}", "Get a skill source and entries"], ["post", "/api/companies/{companyId}/skill-sources/discover", "Discover and validate repository skills"], @@ -12387,12 +12392,14 @@ for (const [method, path, summary] of [ ["delete", "/api/companies/{companyId}/skill-sources/{sourceId}", "Disconnect a skill source and retain installed skills"], ] as const) registerCurrentRoute({ method, path, tags: ["skills"], summary, - ...(method === "patch" ? { body: skillSourceSelectionSchema } + ...(method === "patch" && path.endsWith("/settings") ? { body: skillSourceSettingsSchema } + : method === "post" && path.endsWith("/adopt-project") ? { body: projectSkillSourceAdoptSchema } + : method === "patch" ? { body: skillSourceSelectionSchema } : method === "post" && path.endsWith("/discover") ? { body: skillSourceDiscoverySchema } : method === "post" && path.endsWith("/preview") ? { body: skillSourcePreviewSchema } : method === "post" && path.endsWith("/skill-sources") ? { body: skillSourceCreateSchema } : {}), responses: { - [method === "post" && path.endsWith("/skill-sources") ? 201 : 200]: path.endsWith("/discover") ? { + [path.endsWith("/adopt-project") ? 202 : method === "post" && path.endsWith("/skill-sources") ? 201 : 200]: path.endsWith("/discover") ? { description: "JSON discovery by default. Accept: application/x-ndjson streams progress (phase, measured Git download percentages, and package/file counts), candidate metadata, then complete with discovery. An error event terminates a failed scan; partial candidates cannot be imported. Disconnecting cancels further provider reads.", content: { "application/json": { schema: z.unknown() }, "application/x-ndjson": { schema: z.string() } }, } : r.ok(), diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index edfb4a2f127..d7d29b2cc70 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -1,3 +1,4 @@ +import { skillAccessRequestContext } from "../services/skill-access.js"; import { createHash } from "node:crypto"; import { and, eq, sql } from "drizzle-orm"; import { activityLog } from "@paperclipai/db"; @@ -58,6 +59,7 @@ const SHARED_WORKSPACE_STOP_AND_RESTART_ACTIONS = new Set(["stop", "restart"]); export function projectRoutes(db: Db) { const router = Router(); + router.use(skillAccessRequestContext); const svc = projectService(db); async function repositoryViewer(req: Request) { @@ -167,6 +169,16 @@ export function projectRoutes(db: Db) { return false; } + async function assertProjectWorkspaceWriteAllowed(req: Request, res: Response, project: { id: string; companyId: string }) { + const assigneeAgentId = req.actor.type === "agent" ? req.actor.agentId ?? null : null; + const decision = await access.decide({ actor: req.actor, action: "project:write_workspace", + resource: { type: "issue", companyId: project.companyId, projectId: project.id, assigneeAgentId }, + scope: { projectId: project.id, assigneeAgentId } }); + if (decision.allowed) return true; + res.status(403).json({ error: decision.explanation }); + return false; + } + async function assertCanManageProjectPrivacy(req: Request, res: Response, project: { companyId: string; privacyOwnerUserId?: string | null; personalOwnerUserId?: string | null; }) { @@ -286,6 +298,8 @@ export function projectRoutes(db: Db) { assertBoard(req); const project = await getAccessibleResource(req, res, svc.getById(req.params.id as string), "Project not found"); if (!project) return; + if (!(await assertProjectReadAllowed(req, res, project))) return; + if (!(await assertProjectWorkspaceWriteAllowed(req, res, project))) return; const repositories = await selectedRepositories(req, project.companyId, req.body.repositoryIds, project.workspaces); const updated = await svc.replaceRepositories(project.id, repositories); const actor = getActorInfo(req); @@ -600,6 +614,7 @@ export function projectRoutes(db: Db) { const existing = await getAccessibleResource(req, res, svc.getById(id), "Project not found"); if (!existing) return; if (!(await assertProjectReadAllowed(req, res, existing))) return; + if (!(await assertProjectWorkspaceWriteAllowed(req, res, existing))) return; assertNoAgentHostWorkspaceCommandMutation( req, collectProjectWorkspaceCommandPaths(req.body), @@ -640,6 +655,7 @@ export function projectRoutes(db: Db) { const existing = await getAccessibleResource(req, res, svc.getById(id), "Project not found"); if (!existing) return; if (!(await assertProjectReadAllowed(req, res, existing))) return; + if (!(await assertProjectWorkspaceWriteAllowed(req, res, existing))) return; assertNoAgentHostWorkspaceCommandMutation( req, collectProjectWorkspaceCommandPaths(req.body), @@ -962,6 +978,7 @@ export function projectRoutes(db: Db) { const existing = await getAccessibleResource(req, res, svc.getById(id), "Project not found"); if (!existing) return; if (!(await assertProjectReadAllowed(req, res, existing))) return; + if (!(await assertProjectWorkspaceWriteAllowed(req, res, existing))) return; const workspace = await svc.removeWorkspace(id, workspaceId); if (!workspace) { res.status(404).json({ error: "Project workspace not found" }); diff --git a/server/src/services/activity.ts b/server/src/services/activity.ts index 81c2ebdc1a6..db231abbb3a 100644 --- a/server/src/services/activity.ts +++ b/server/src/services/activity.ts @@ -1,3 +1,4 @@ +import { skillAccessService } from "./skill-access.js"; import { credentialAccessConnectionNameSql } from "./credential-access-visibility.js"; import { executionProjectionsForRuns } from "./execution-projection.js"; import { and, asc, desc, eq, inArray, isNull, or, sql, type SQL } from "drizzle-orm"; @@ -354,8 +355,8 @@ export function activityService(db: Db) { } return { - list: (filters: ActivityFilters) => { - const conditions = [eq(activityLog.companyId, filters.companyId)]; + list: async (filters: ActivityFilters) => { + const conditions = [eq(activityLog.companyId, filters.companyId), await skillAccessService(db).activityCondition(filters.companyId)]; const limit = normalizeActivityLimit(filters.limit); if (filters.agentId) { diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 8dca564153b..c3509bcfd17 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -3903,6 +3903,9 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const agentSelection = resolvePortableExportAgentSelection(allAgentRows, input.agents, include.agents); const companySkillRowsRaw = include.skills ? await companySkills.listFull(companyId) : []; + if (companySkillRowsRaw.some(skill => skill.accessSourceId)) { + throw unprocessable("Project-restricted skills cannot be exported until the package can preserve their project access."); + } const managedSkillRows = companySkillRowsRaw.filter((skill) => managedSkillIds.has(skill.id)); const companySkillRows = companySkillRowsRaw.filter((skill) => !managedSkillIds.has(skill.id)); warnings.push(...agentSelection.warnings); diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 8b3810722da..c6da9f8bb86 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -1,3 +1,5 @@ +import { retainProjectSkillAccess, skillAccessService } from "./skill-access.js"; +import type { AuthorizationActor } from "./authorization.js"; import { updateAgentConfigurationInTransaction } from "./agent-configuration-transaction.js"; import { logger } from "../middleware/logger.js"; import { removeRuntimeSkillCache, resolveRuntimeSkillCache, runtimeSkillCacheSpec } from "./runtime-skill-cache.js"; @@ -5,9 +7,9 @@ import { createHash, randomUUID } from "node:crypto"; import { promises as fs } from "node:fs"; import path from "node:path"; import os from "node:os"; -import { skillFileBytes, snapshotFile, assertSkillSnapshotPath } from "./skill-snapshot.js"; +import { skillFileBytes, snapshotFile, assertSkillSnapshotPath, skillSnapshotHash } from "./skill-snapshot.js"; import { fileURLToPath } from "node:url"; -import { and, asc, desc, eq, inArray, isNull, lt, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, isNull, isNotNull, lt, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents as agentsTable, @@ -29,7 +31,8 @@ import { issueThreadInteractions, issueWorkProducts, } from "@paperclipai/db"; -import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference, resolvePaperclipDesiredSkillNames, + resolveLegacyPaperclipDesiredSkillNames } from "@paperclipai/adapter-utils/server-utils"; import { REPOSITORY_CONTENT_DIR, repositorySkillProjection } from "./skill-repository-package.js"; import type { PaperclipDesiredSkillEntry, PaperclipSkillEntry } from "@paperclipai/adapter-utils/server-utils"; import type { @@ -240,6 +243,7 @@ type ImportedSkillPersistValues = Pick< | "name" | "description" | "markdown" + | "accessSourceId" | "sourceType" | "sourceLocator" | "sourceRef" @@ -377,6 +381,10 @@ export type ProjectSkillScanTarget = { type RuntimeSkillEntryOptions = { materializeMissing?: boolean; versionSelections?: Map; + selectionConfig?: Record; + includeLegacy?: boolean; + onlyKeys?: Set; + snapshotLatest?: boolean; }; type SkillActor = { @@ -440,6 +448,7 @@ function selectCompanySkillColumns() { authorName: companySkills.authorName, homepageUrl: companySkills.homepageUrl, categories: companySkills.categories, + accessSourceId: companySkills.accessSourceId, sharingScope: companySkills.sharingScope, publicShareToken: companySkills.publicShareToken, forkedFromSkillId: companySkills.forkedFromSkillId, @@ -2953,7 +2962,8 @@ async function listLastEditorsBySkillId( return editors; } -export function companySkillService(db: Db) { +export function companySkillService(db: Db, options: { actor?: AuthorizationActor } = {}) { + const visibility = (database: DbOrTransaction = db) => skillAccessService(database as Db, options.actor); const folderSvc = folderService(db); const agents = agentService(db); const projects = projectService(db); @@ -2986,6 +2996,25 @@ export function companySkillService(db: Db) { remediation: "Import from a configured Paperclip workspace or the company managed-skill directory.", }); } + // A filesystem import cannot discard the provenance of installed private + // snapshots or their materialized copies, even for an authorized reader. + const restricted = await db.select({ id: companySkills.id, key: companySkills.key, slug: companySkills.slug, + sourceType: companySkills.sourceType, sourceLocator: companySkills.sourceLocator }).from(companySkills).where(and( + eq(companySkills.companyId, companyId), isNotNull(companySkills.accessSourceId))); + for (const skill of restricted) { + const localRoot = skill.sourceType === "local_path" || skill.sourceType === "catalog" ? normalizeSourceLocatorDirectory(skill.sourceLocator) : null; + const protectedRoots = [path.join(resolveManagedSkillsRoot(companyId), "__versions__", skill.id), + resolveRuntimeSkillMaterializedPath(companyId, skill), localRoot].filter((root): root is string => Boolean(root)); + for (const root of protectedRoots) { + const canonical = await fs.realpath(root).catch(() => null); + if (canonical && (sourceRealPath === canonical || sourceRealPath.startsWith(`${canonical}${path.sep}`) + || canonical.startsWith(`${sourceRealPath}${path.sep}`))) { + await visibility().assertSkill(companyId, skill.id); + throw unprocessable("Project-derived skill files cannot be imported as a company-wide local source. Fork the installed skill to retain project access.", + { code: "skill_project_provenance_required" }); + } + } + } } async function ensureBundledSkills(companyId: string) { @@ -3211,6 +3240,8 @@ export function companySkillService(db: Db) { } async function ensureSkillInventoryCurrent(companyId: string) { + // Denied reads must not perform the lazy bundled-skill reconciliation. + if (!visibility().canReadCompany(companyId)) return; const existingRefresh = skillInventoryRefreshPromises.get(companyId); if (existingRefresh) { await existingRefresh; @@ -3243,6 +3274,7 @@ export function companySkillService(db: Db) { } async function list(companyId: string, query: CompanySkillListQuery = {}): Promise { + if (!visibility().canReadCompany(companyId)) return []; await ensureSkillInventoryCurrent(companyId); const [dbRows, folderListing] = await Promise.all([db .select({ @@ -3265,6 +3297,7 @@ export function companySkillService(db: Db) { authorName: companySkills.authorName, homepageUrl: companySkills.homepageUrl, categories: companySkills.categories, + accessSourceId: companySkills.accessSourceId, sharingScope: companySkills.sharingScope, publicShareToken: companySkills.publicShareToken, forkedFromSkillId: companySkills.forkedFromSkillId, @@ -3278,7 +3311,7 @@ export function companySkillService(db: Db) { updatedAt: companySkills.updatedAt, }) .from(companySkills) - .where(eq(companySkills.companyId, companyId)) + .where(and(eq(companySkills.companyId, companyId), await visibility().condition(companyId))) .orderBy(asc(companySkills.name), asc(companySkills.key)) .then((entries) => entries.map((entry) => toCompanySkillListRow(entry as CompanySkillListDbRow))), folderSvc.list(companyId, "skill"), @@ -3348,11 +3381,12 @@ export function companySkillService(db: Db) { } async function listFull(companyId: string): Promise { + if (!visibility().canReadCompany(companyId)) return []; await ensureSkillInventoryCurrent(companyId); const rows = await db .select(selectCompanySkillColumns()) .from(companySkills) - .where(eq(companySkills.companyId, companyId)) + .where(and(eq(companySkills.companyId, companyId), await visibility().condition(companyId))) .orderBy(asc(companySkills.name), asc(companySkills.key)); return rows.map((row) => toCompanySkill(row)); } @@ -3378,7 +3412,7 @@ export function companySkillService(db: Db) { slug: companySkills.slug, }) .from(companySkills) - .where(eq(companySkills.companyId, companyId)); + .where(and(eq(companySkills.companyId, companyId), await visibility().condition(companyId))); return rows as CompanySkillReferenceRow[]; } @@ -3386,7 +3420,7 @@ export function companySkillService(db: Db) { const row = await database .select(selectCompanySkillColumns()) .from(companySkills) - .where(and(eq(companySkills.companyId, companyId), eq(companySkills.id, id))) + .where(and(eq(companySkills.companyId, companyId), eq(companySkills.id, id), await visibility(database).condition(companyId))) .then((rows) => rows[0] ?? null); return row ? enrichFolderPath(companyId, toCompanySkill(row)) : null; } @@ -3395,7 +3429,7 @@ export function companySkillService(db: Db) { const row = await database .select(selectCompanySkillColumns()) .from(companySkills) - .where(and(eq(companySkills.companyId, companyId), eq(companySkills.key, key))) + .where(and(eq(companySkills.companyId, companyId), eq(companySkills.key, key), await visibility(database).condition(companyId))) .then((rows) => rows[0] ?? null); return row ? enrichFolderPath(companyId, toCompanySkill(row)) : null; } @@ -3404,7 +3438,7 @@ export function companySkillService(db: Db) { const rows = await db .select(selectCompanySkillColumns()) .from(companySkills) - .where(and(eq(companySkills.companyId, companyId), eq(companySkills.slug, slug))); + .where(and(eq(companySkills.companyId, companyId), eq(companySkills.slug, slug), await visibility().condition(companyId))); return rows.length === 1 ? enrichFolderPath(companyId, toCompanySkill(rows[0]!)) : null; } @@ -3421,6 +3455,7 @@ export function companySkillService(db: Db) { } async function getVersion(companyId: string, skillId: string, versionId: string): Promise { + if (!await getById(companyId, skillId)) return null; const row = await db .select() .from(companySkillVersions) @@ -3993,16 +4028,17 @@ export function companySkillService(db: Db) { await ensureSkillInventoryCurrent(companyId); const source = await getById(companyId, skillId); if (!source) throw notFound("Skill not found"); - const existing = await listFull(companyId); + if (source.accessSourceId && input.sharingScope === "public_link") throw forbidden("Project skills cannot be published through a public link."); + const existing = await db.select({ slug: companySkills.slug }).from(companySkills) + .where(eq(companySkills.companyId, companyId)); const usedSlugs = new Set(existing.map((skill) => normalizeSkillSlug(skill.slug) ?? skill.slug)); const forkSlug = uniqueSkillSlug(normalizeSkillSlug(input.slug ?? `${source.slug}-fork`) ?? `${source.slug}-fork`, usedSlugs); const forkName = input.name?.trim() || `${source.name} Fork`; const forkKey = `company/${companyId}/${forkSlug}`; const plannedReassignments = await planSkillReassignments(companyId, source, forkKey, input.reassignAgentIds); const managedRoot = resolveManagedSkillsRoot(companyId); - const forkDir = path.resolve(managedRoot, forkSlug); - await fs.rm(forkDir, { recursive: true, force: true }); - await fs.mkdir(forkDir, { recursive: true }); + await fs.mkdir(managedRoot, { recursive: true }); + const forkDir = await fs.mkdtemp(path.join(managedRoot, `${forkSlug}-fork-`)); const sourceVersion = await getCurrentVersion(source); const repositoryFiles = sourceVersion?.repositorySnapshotId ? await runtimeVersionInventory(companyId, sourceVersion) : null; for (const entry of repositoryFiles ?? source.fileInventory) { @@ -4019,7 +4055,9 @@ export function companySkillService(db: Db) { } const inventory = await collectLocalSkillInventory(forkDir); const parsed = parseFrontmatterMarkdown(markdown); + const accessSourceId = await retainProjectSkillAccess(db, companyId, source.accessSourceId); const metadata = { + accessSourceId, sourceKind: "managed_local", forkedFromSkillId: source.id, forkedFromCompanyId: source.companyId, @@ -4044,6 +4082,7 @@ export function companySkillService(db: Db) { await db .update(companySkills) .set({ + accessSourceId, iconUrl: source.iconUrl, color: source.color, tagline: source.tagline, @@ -4437,6 +4476,7 @@ export function companySkillService(db: Db) { const skill = await getById(companyId, skillId); if (!skill) throw notFound("Skill not found"); + if (skill.accessSourceId && input.sharingScope === "public_link") throw forbidden("Project skills cannot be published through a public link."); const sharingScope = Object.prototype.hasOwnProperty.call(input, "sharingScope") ? normalizeMutableSharingScope(input.sharingScope) : null; @@ -4495,6 +4535,9 @@ export function companySkillService(db: Db) { if (input.forkedFromSkillId && !forkSource) { throw notFound("Fork source skill not found"); } + if (forkSource?.accessSourceId && input.sharingScope === "public_link") { + throw forbidden("Project skills cannot be published through a public link."); + } const forkVersion = forkSource ? await getCurrentVersion(forkSource) : null; const repositoryFiles = forkVersion?.repositorySnapshotId ? await runtimeVersionInventory(companyId, forkVersion) : null; const sharingScope = normalizeMutableSharingScope(input.sharingScope) ?? "company"; @@ -4576,8 +4619,10 @@ export function companySkillService(db: Db) { ? await collectLocalSkillInventory(skillDir) : [{ path: "SKILL.md", kind: "skill" as const }]; const parsed = parseFrontmatterMarkdown(markdown); + const accessSourceId = await retainProjectSkillAccess(db, companyId, forkSource?.accessSourceId); const metadata = { sourceKind: "managed_local", + ...(accessSourceId ? { accessSourceId } : {}), ...(forkSource ? { forkedFromSkillId: forkSource.id, forkedFromCompanyId: forkSource.companyId, @@ -6015,7 +6060,7 @@ export function companySkillService(db: Db) { options: RuntimeSkillEntryOptions, ): Promise { const selectedVersionId = options.versionSelections?.get(skill.key) - ?? (skill.metadata?.skillSourceId && skill.metadata?.snapshotHash ? skill.currentVersionId : null); + ?? (options.snapshotLatest || (skill.metadata?.skillSourceId && skill.metadata?.snapshotHash) || skill.accessSourceId ? skill.currentVersionId : null); if (selectedVersionId) { const versionPath = path.resolve(resolveManagedSkillsRoot(companyId), "__versions__", skill.id, selectedVersionId); const version = await getVersion(companyId, skill.id, selectedVersionId); @@ -6099,17 +6144,46 @@ export function companySkillService(db: Db) { options: RuntimeSkillEntryOptions = {}, ): Promise { const skills = await listFull(companyId); + const selectedKeys = options.onlyKeys ?? (options.selectionConfig ? new Set( + (options.includeLegacy ? resolveLegacyPaperclipDesiredSkillNames : resolvePaperclipDesiredSkillNames)( + options.selectionConfig, skills.map(skill => ({ key: skill.key, runtimeName: buildSkillRuntimeName(skill.key, skill.slug) }))), + ) : null); const out: PaperclipSkillEntry[] = []; - for (const skill of skills) { - const sourceResolution = await resolveRuntimeSkillSource(companyId, skill, options); + for (let skill of skills) { + if (selectedKeys && !selectedKeys.has(skill.key)) continue; + let installedResolution: RuntimeSkillSourceResolution | null = null; + const unpinned = !options.versionSelections?.get(skill.key); + const installedDirectory = options.snapshotLatest && unpinned && (skill.sourceType === "local_path" || skill.sourceType === "catalog") + ? await resolveExistingSkillDirectory(normalizeSkillDirectory(skill)) : null; + if (options.snapshotLatest && unpinned && (!skill.currentVersionId || installedDirectory)) { + // Capture installed bytes only. Older remote imports can have no + // version yet; execution must not fetch their missing files upstream. + const installed: RuntimeSkillSourceResolution | null = installedDirectory ? { status: "available", source: installedDirectory } + : await resolveRuntimeSkillSource(companyId, skill, { ...options, snapshotLatest: false, materializeMissing: false }); + const inventory = installed?.status === "available" + ? await collectVersionFileInventoryFromDirectory(installed.source) + : skill.fileInventory.length === 1 && skill.fileInventory[0]?.path === "SKILL.md" && skill.markdown + ? [{ path: "SKILL.md", kind: "skill" as const, content: skill.markdown }] : null; + if (inventory) { + const previous = skill.currentVersionId ? await getVersion(companyId, skill.id, skill.currentVersionId) : null; + if (!previous || skillSnapshotHash(previous.fileInventory) !== skillSnapshotHash(inventory)) { + const version = await createVersion(companyId, skill.id, { label: "Execution snapshot" }, { type: "system" }, + { skipInventoryRefresh: true, skill, fileInventory: inventory }); + skill = { ...skill, currentVersionId: version.id }; + } + } else installedResolution = installed; + } + const sourceResolution = installedResolution ?? await resolveRuntimeSkillSource(companyId, skill, { + ...options, ...(options.snapshotLatest ? { materializeMissing: false } : {}), + }); if (!sourceResolution) continue; out.push({ key: skill.key, runtimeName: buildSkillRuntimeName(skill.key, skill.slug), source: sourceResolution.source, - versionId: options.versionSelections?.get(skill.key) ?? (skill.metadata?.skillSourceId && skill.metadata?.snapshotHash ? skill.currentVersionId : null), + versionId: options.versionSelections?.get(skill.key) ?? (options.snapshotLatest || (skill.metadata?.skillSourceId && skill.metadata?.snapshotHash) || skill.accessSourceId ? skill.currentVersionId : null), currentVersionId: skill.currentVersionId, sourceStatus: sourceResolution.status, missingDetail: sourceResolution.status === "missing" ? sourceResolution.detail : null, @@ -6308,6 +6382,7 @@ export function companySkillService(db: Db) { name: skill.name, description: skill.description, markdown: skill.markdown, + accessSourceId: typeof skill.metadata?.accessSourceId === "string" ? skill.metadata.accessSourceId : existing?.accessSourceId ?? null, sourceType: skill.sourceType, sourceLocator: skill.sourceLocator, sourceRef: skill.sourceRef, diff --git a/server/src/services/github-skill-source.ts b/server/src/services/github-skill-source.ts index 6b7951bf5c4..668463878a2 100644 --- a/server/src/services/github-skill-source.ts +++ b/server/src/services/github-skill-source.ts @@ -14,9 +14,10 @@ export interface GitHubRead { withScan?: (operation: () => Promise) => Promise; openSnapshot: (input: { repositoryUrl: string; ref: string; commitSha?: string }, options?: GitSkillSnapshotOptions) => Promise; readonly connectionId?: string | null; + readonly grantId?: string | null; } export type DiscoveredSkill = SkillSourceCandidate & { files: CompanySkillVersionFileInventoryEntry[] }; -export type ScannedSkillSource = SkillSourceDiscovery & { skills: DiscoveredSkill[]; defaultBranch: string; repositoryFiles?: CompanySkillVersionFileInventoryEntry[] }; +export type ScannedSkillSource = SkillSourceDiscovery & { skills: DiscoveredSkill[]; defaultBranch: string; treeSha?: string; downloadBytes?: number; scanBytes?: number; repositoryFiles?: CompanySkillVersionFileInventoryEntry[] }; const MAX_FILE_BYTES = 1024 * 1024; const MAX_SCAN_BYTES = 100 * 1024 * 1024; const MAX_SCAN_PACKAGES = 1_000; @@ -42,6 +43,28 @@ export function scanGitHubSkills(input: SkillScanInput, providerRead: GitHubRead return providerRead.withScan ? providerRead.withScan(scan) : scan(); } +/** Oversized repositories may still contain valid, bounded skill packages. */ +export async function scanGitHubSkillsWithPackageFallback(input: SkillScanInput, providerRead: GitHubRead, options: SkillScanOptions = {}): Promise { + try { return await scanGitHubSkills(input, providerRead, options); } + catch (error) { + if (input.packageMode !== 'repository' || !(error instanceof Error) || !/^Repository package exceeds/.test(error.message)) throw error; + options.signal?.throwIfAborted(); + const scan = await scanGitHubSkills({ ...input, packageMode: 'skills' }, providerRead, options); + rejectIncompleteSkillPackages(scan); + scan.warnings.push('Only skill packages were imported because the repository exceeds the supported package size.'); + return scan; + } +} + +export function rejectIncompleteSkillPackages(scan: ScannedSkillSource): void { + for (const skill of scan.skills) { + if (!skill.error && skill.inspection?.references.length) { + skill.error = 'This skill depends on files that its imported package cannot provide. Its dependencies could not be preserved. Import a supported repository package or include the dependencies in this skill package.'; + } + } + scan.candidates = scan.skills.map(({ files: _files, ...candidate }) => candidate); +} + async function scanRepository(input: SkillScanInput, providerRead: GitHubRead, options: SkillScanOptions): Promise { const read = async (apiPath: string) => { options.signal?.throwIfAborted(); @@ -249,7 +272,9 @@ async function scanRepository(input: SkillScanInput, providerRead: GitHubRead, o if (skill.inspection) skill.inspection.references = skill.inspection.references.filter(ref => !included.has(ref.resolvedPath)); } } - return { ...(input.packageMode ? { packageMode: input.packageMode } : {}), ...(repositoryPackage ? { repositoryPackage } : {}), + return { treeSha: snapshot.treeSha, downloadBytes: snapshot.downloadBytes, + scanBytes: Math.min(MAX_SCAN_BYTES, snapshot.entries.reduce((sum, entry) => sum + (entry.type === 'blob' ? entry.size ?? MAX_SCAN_BYTES : 0), 0)), + ...(input.packageMode ? { packageMode: input.packageMode } : {}), ...(repositoryPackage ? { repositoryPackage } : {}), ...(repositoryFiles && options.retainFiles !== false ? { repositoryFiles } : {}), connectionId: providerRead.connectionId ?? null, repositoryId: String(repo.id), repositoryUrl: `https://github.com/${repo.full_name.toLowerCase()}`, fullName: repo.full_name, trackingRef, commitSha: commit.sha, defaultBranch: repo.default_branch, candidates: skills.map(({ files: _files, ...candidate }) => candidate), warnings, skills }; diff --git a/server/src/services/heartbeat/execution-configuration.ts b/server/src/services/heartbeat/execution-configuration.ts index 99bff95999b..66408dc1032 100644 --- a/server/src/services/heartbeat/execution-configuration.ts +++ b/server/src/services/heartbeat/execution-configuration.ts @@ -1,7 +1,10 @@ +import { withSkillActor } from "../skill-access.js"; +import type { AuthorizationActor } from "../authorization.js"; import { eq, sql } from "drizzle-orm"; import { heartbeatRuns, type Db, type agents, type projects } from "@paperclipai/db"; import type { AiConnectionBinding } from "@paperclipai/shared"; -import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { readPaperclipSkillSyncPreference, resolvePaperclipDesiredSkillNames, resolveLegacyPaperclipDesiredSkillNames, + writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; import { parseObject } from "../../adapters/utils.js"; import { logger } from "../../middleware/logger.js"; import { AI_CONNECTION_BUSY_RETRY_REASON, extractWakeCommentIds, isNonAssigneeWorkspaceBusyRetry } from "../../modules/run-dispatch/index.js"; @@ -88,12 +91,15 @@ export async function prepareHeartbeatExecutionConfiguration(db: Db, input: Hear ); const executionRunConfig = stripWorkspaceRuntimeFromExecutionRunConfig(mergedConfig); + const skillActor: AuthorizationActor = { type: "agent", agentId: agent.id, companyId: agent.companyId, + runId: run.id, source: "agent_jwt", onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId }; const runScopedMentionedSkillKeys = await resolveRunScopedMentionedSkillKeys({ db, companyId: agent.companyId, issueId, commentIds: extractWakeCommentIds(context), + actor: skillActor, }); const runScopedSkillKeys = acceptedPlanContinuationWake && @@ -199,18 +205,50 @@ export async function prepareHeartbeatExecutionConfiguration(db: Db, input: Hear ); const nativeRunnerPreparationSpans: NativeRunHistoricalSpan[] = []; const skillsPrepareStartedAtMs = Date.now(); + let skillDeliveryConfig = effectiveResolvedConfig; const runtimeSkillEntries = await (async () => { try { - return await companySkills.listRuntimeSkillEntries(agent.companyId, { - versionSelections: skillVersionSelectionMap( - runtimeSkillPreference.desiredSkillEntries, - { - versionPinsEnabled: - resolvedInstanceSettings.experimental.enableBetaSkills === - true, - }, - ), - }); + const storedVersions = parseObject(run.contextSnapshot?.skillVersions); + const hasStoredVersions = Object.prototype.hasOwnProperty.call(run.contextSnapshot ?? {}, "skillVersions"); + const versionSelections = hasStoredVersions + ? new Map(Object.entries(storedVersions).filter((entry): entry is [string, string | null] => entry[1] === null || typeof entry[1] === "string")) + : skillVersionSelectionMap(runtimeSkillPreference.desiredSkillEntries, { + versionPinsEnabled: resolvedInstanceSettings.experimental.enableBetaSkills === true, + }); + const entries = await withSkillActor(skillActor, () => companySkills.listRuntimeSkillEntries(agent.companyId, { + versionSelections, snapshotLatest: true, + ...(hasStoredVersions && run.contextSnapshot?.skillSelectionSnapshot === true + ? { onlyKeys: new Set(Object.keys(storedVersions)) } + : { selectionConfig: effectiveResolvedConfig, includeLegacy: agent.adapterType !== "paperclip_runner" }), + })); + const available = hasStoredVersions ? entries.filter(entry => Object.prototype.hasOwnProperty.call(storedVersions, entry.key)) : entries; + const availableKeys = new Set(available.map(entry => entry.key)); + const desiredKeys = hasStoredVersions && run.contextSnapshot?.skillSelectionSnapshot === true + ? Object.keys(storedVersions) + : agent.adapterType === "paperclip_runner" + ? resolvePaperclipDesiredSkillNames(effectiveResolvedConfig, available) + : resolveLegacyPaperclipDesiredSkillNames(effectiveResolvedConfig, available); + const selectedKeys = new Set(desiredKeys.filter(key => availableKeys.has(key))); + const resolved = available.filter(entry => selectedKeys.has(entry.key)); + // Optional assignments can outlive project membership. Drop inaccessible + // assignments from delivery; explicit task mentions remain errors below. + skillDeliveryConfig = writePaperclipSkillSyncPreference(effectiveResolvedConfig, + resolved.map(entry => ({ key: entry.key, versionId: entry.versionId ?? null }))); + for (const key of runScopedMentionedSkillKeys) { + const entry = resolved.find(candidate => candidate.key === key); + if (!entry || entry.sourceStatus === "missing") { + throw new ConfigurationIncompleteFailure("A referenced skill is unavailable. Restore its installed version or update the task's skill reference.", { + configurationIncomplete: { reason: "skill_unavailable", skillKey: key, actionUrl: "/skills" }, + }); + } + } + if (!hasStoredVersions) { + const skillVersions = Object.fromEntries(resolved.map(entry => [entry.key, entry.versionId ?? entry.currentVersionId ?? null])); + context.skillVersions = skillVersions; + context.skillSelectionSnapshot = true; + await db.update(heartbeatRuns).set({ contextSnapshot: sql`coalesce(${heartbeatRuns.contextSnapshot}, '{}'::jsonb) || ${JSON.stringify({ skillVersions, skillSelectionSnapshot: true })}::jsonb` }).where(eq(heartbeatRuns.id, run.id)); + } + return resolved; } catch (error) { if (agent.adapterType === "paperclip_runner") { await recordFailedSkillPreparation({ @@ -231,7 +269,7 @@ export async function prepareHeartbeatExecutionConfiguration(db: Db, input: Hear endedAtMs: Date.now(), }); const connectorAssignments = await resolveConnectorAssignments(db, { companyId: agent.companyId, agentId: agent.id, runId: run.id, issueId: typeof context.issueId === "string" ? context.issueId : undefined }); - const connectorSkillConfig = await applyConnectorSkills(effectiveResolvedConfig, runtimeSkillEntries, connectorAssignments); + const connectorSkillConfig = await applyConnectorSkills(skillDeliveryConfig, runtimeSkillEntries, connectorAssignments); // Both CLI adapters and native context materialization use the same resolved set. runtimeSkillEntries.splice(0, runtimeSkillEntries.length, ...connectorSkillConfig.paperclipRuntimeSkills); const connectorDelivery = await prepareConnectorSkillDelivery(connectorSkillConfig, agent.adapterType); diff --git a/server/src/services/heartbeat/run-preparation.ts b/server/src/services/heartbeat/run-preparation.ts index ad4fbd646d7..1bfde2c85cf 100644 --- a/server/src/services/heartbeat/run-preparation.ts +++ b/server/src/services/heartbeat/run-preparation.ts @@ -1,3 +1,5 @@ +import { skillAccessService } from "../skill-access.js"; +import type { AuthorizationActor } from "../authorization.js"; import { spekoToolsForSession } from "../voice/speko-agent-tools.js"; import { githubConfiguredInstructionSources } from "../chat-github-review-policy.js"; import { CONFIGURED_ENVIRONMENT_KEYS } from "../../vendor/paperclip-runner/index.js"; @@ -727,6 +729,7 @@ export async function resolveRunScopedMentionedSkillKeys(input: { companyId: string; issueId: string | null; commentIds?: string[]; + actor?: AuthorizationActor; }): Promise { if (!input.issueId) return []; @@ -801,6 +804,14 @@ export async function resolveRunScopedMentionedSkillKeys(input: { inArray(companySkillsTable.id, mentionedSkillIds), ), ); + for (const skillId of mentionedSkillIds) { + try { await skillAccessService(input.db, input.actor).assertSkill(input.companyId, skillId); } + catch { + throw new ConfigurationIncompleteFailure("A referenced skill is unavailable or you do not have project access. Update the task's skill reference or project access.", { + configurationIncomplete: { reason: "skill_unavailable", companyId: input.companyId, actionUrl: "/skills" }, + }); + } + } const skillKeyById = new Map(skillRows.map((row) => [row.id, row.key])); return mentionedSkillIds .map((skillId) => skillKeyById.get(skillId) ?? null) diff --git a/server/src/services/heartbeat/runtime-selection.test.ts b/server/src/services/heartbeat/runtime-selection.test.ts index f5f2c8bb706..b15146b4e8c 100644 --- a/server/src/services/heartbeat/runtime-selection.test.ts +++ b/server/src/services/heartbeat/runtime-selection.test.ts @@ -157,6 +157,28 @@ describe.skipIf(!support.supported)("heartbeat runtime selection boundary", () = expect((await read(run.id)).runnerProfileJson).toEqual(saved.runnerProfileJson); }); + it("rejects resume after skill access revocation without rewriting the admitted revision", async () => { + const { input, run } = await fixture(); + const key = "company/test/private-skill", versionId = randomUUID(); + await db.update(heartbeatRuns).set({ contextSnapshot: { skillVersions: { [key]: versionId } } }).where(eq(heartbeatRuns.id, run.id)); + input.run = await read(run.id); + vi.mocked(buildNativeRuntimeContext).mockImplementationOnce(async () => { + const context = nativeRuntimeContextFixture(); + context.instructions.bundle.rootPath = instructionRoot; + context.skills = [{ key, runtimeName: "private-skill", versionId, bundle: { ...context.instructions.bundle } }]; + return { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) }; + }); + const selected = await selectHeartbeatRuntime(db, input); + if (!selected.selected || !selected.nativeExecution) throw new Error("Expected native selection"); + const saved = await read(run.id); + input.run = saved; + input.session.persistedNativeExecutionInput = selected.nativeExecution; + input.config.runtimeSkillEntries = []; + await expect(selectHeartbeatRuntime(db, input)).rejects.toThrow("Project skill access changed"); + expect((await read(run.id)).runnerProfileJson).toEqual(saved.runnerProfileJson); + expect((await read(run.id)).contextSnapshot?.skillVersions).toEqual({ [key]: versionId }); + }); + it.each(["cancelled", "startup_cancellation"])("does not create a native coordinator when %s wins preparation", async winner => { const { input, run } = await fixture(); input.beforeNativeRuntimeSelection = async () => { diff --git a/server/src/services/heartbeat/runtime-selection.ts b/server/src/services/heartbeat/runtime-selection.ts index c179e948bcf..7b745c75691 100644 --- a/server/src/services/heartbeat/runtime-selection.ts +++ b/server/src/services/heartbeat/runtime-selection.ts @@ -536,6 +536,16 @@ export async function selectHeartbeatRuntime(db: Db, input: HeartbeatRuntimeSele throw new Error( "native_execution_input_persisted_binding_mismatch", ); + if ("runtimeContext" in nativeExecution) { + const savedVersions = parseObject(run.contextSnapshot?.skillVersions); + const accessibleKeys = new Set(runtimeSkillEntries.filter(entry => entry.sourceStatus !== "missing").map(entry => entry.key)); + if (nativeExecution.runtimeContext.skills.some(skill => + Object.prototype.hasOwnProperty.call(savedVersions, skill.key) && !accessibleKeys.has(skill.key))) { + throw new ConfigurationIncompleteFailure("Project skill access changed while this execution was suspended. Restore access or start a new execution.", { + configurationIncomplete: { reason: "skill_access_changed", actionUrl: "/skills" }, + }); + } + } // Recover only the originally admitted request. A stored idle // checkpoint can precede an already-started provider turn, so even // apparent idleness is not authority to rewrite its contract. diff --git a/server/src/services/local-ai-browser-login.test.ts b/server/src/services/local-ai-browser-login.test.ts index 5f021570189..9bfdd7434ab 100644 --- a/server/src/services/local-ai-browser-login.test.ts +++ b/server/src/services/local-ai-browser-login.test.ts @@ -63,6 +63,7 @@ function resolveFixtureCodexCommand(home: string): string { describe.skipIf(process.platform === "win32")("local browser subscription login", () => { it.each(["native", "arm32", "musl", "glibc-2.38", "glibc-3.0", "unknown"])("uses the selected Codex command with its credential home and ambient HOME/cwd (%s)", async platform => { const legacy = platform === "arm32"; + if (platform === "native") targetPlatform("linux", "x64"); if (platform === "arm32") targetPlatform("linux", "arm"); if (platform === "musl") targetPlatform("linux", "arm64", false); if (platform === "glibc-2.38") targetPlatform("linux", "arm64", true, "2.38"); @@ -163,6 +164,7 @@ describe.skipIf(process.platform === "win32")("local browser subscription login" }); it("reports a missing terminal prerequisite without exposing spawn details", async () => { + targetPlatform("linux", "x64"); const home = await fakeCli("codex", "exit 0"); process.env.PATH = path.join(root!, "no-python"); const login = startLocalBrowserLogin("openai", home); diff --git a/server/src/services/native-runtime/paperclip-runner-tool-authority.ts b/server/src/services/native-runtime/paperclip-runner-tool-authority.ts index 1e4c7cab6b8..adb4ff71f7e 100644 --- a/server/src/services/native-runtime/paperclip-runner-tool-authority.ts +++ b/server/src/services/native-runtime/paperclip-runner-tool-authority.ts @@ -1,3 +1,4 @@ +import { skillAccessService } from "../skill-access.js"; import { callTaskWorkspaceTool } from "../task-workspace-tools.js"; import { TaskAttachmentReadCache, TASK_ATTACHMENT_DEFINITIONS, TASK_ATTACHMENT_SCHEMAS, listTaskAttachments, readTaskAttachment } from "./runner-task-attachments.js"; import { RUNNER_BRIDGE_SCHEMAS, runnerBridgeDefinitions, readAssignedSkill, executeWorkspaceTool, readWorkspaceUploadFile, settleRunnerBridgeRead, assertRunnerWorkspacePathAllowed } from "./runner-bridge-tools.js"; @@ -55,6 +56,7 @@ import type { Db } from "@paperclipai/db"; import { authUsers, companyMemberships, + companySkills, activityLog, assets, runnerApiResponseReservations, @@ -350,8 +352,19 @@ export class PaperclipRunnerToolAuthority { .innerJoin(authUsers, eq(authUsers.id, companyMemberships.principalId)) .where(and(eq(companyMemberships.companyId, this.binding.companyId), eq(companyMemberships.principalType, "user"), eq(companyMemberships.status, "active"), input.after ? sql`${authUsers.id} > ${input.after}` : undefined)) .orderBy(authUsers.id).limit(input.limit); - if (call.tool === "list_assigned_skills") return this.binding.runtimeContext!.skills.map(skill => ({ key: skill.key, name: skill.runtimeName, versionId: skill.versionId, digest: skill.bundle.digest })); - if (call.tool === "read_assigned_skill") return settleRunnerBridgeRead(() => readAssignedSkill(this.binding.runtimeContext!, input)); + if (call.tool === "list_assigned_skills" || call.tool === "read_assigned_skill") { + const current = await this.#boundContext(); + const visibility = await skillAccessService(this.db, this.#privacyActor(current.run)).condition(this.binding.companyId); + const known = await this.db.select({ key: companySkills.key }).from(companySkills).where(eq(companySkills.companyId, this.binding.companyId)); + const permitted = await this.db.select({ key: companySkills.key }).from(companySkills).where(and(eq(companySkills.companyId, this.binding.companyId), visibility)); + const hidden = new Set(known.filter(skill => !permitted.some(allowed => allowed.key === skill.key)).map(skill => skill.key)); + const savedVersions = current.run.contextSnapshot?.skillVersions; + const permittedKeys = new Set(permitted.map(skill => skill.key)); + const runtimeContext = { ...this.binding.runtimeContext!, skills: this.binding.runtimeContext!.skills.filter(skill => + !hidden.has(skill.key) && !(savedVersions && Object.prototype.hasOwnProperty.call(savedVersions, skill.key) && !permittedKeys.has(skill.key))) }; + if (call.tool === "list_assigned_skills") return runtimeContext.skills.map(skill => ({ key: skill.key, name: skill.runtimeName, versionId: skill.versionId, digest: skill.bundle.digest })); + return settleRunnerBridgeRead(() => readAssignedSkill(runtimeContext, input)); + } const paths: Record = { get_task: ["GET", "/api/issues/{id}"], comment_on_task: ["POST", "/api/issues/{id}/comments"], list_task_documents: ["GET", "/api/issues/{id}/documents"], read_task_document: ["GET", "/api/issues/{id}/documents/{key}"], write_task_document: ["PUT", "/api/issues/{id}/documents/{key}"] }; const [method, path] = paths[call.tool]!; diff --git a/server/src/services/project-skill-source-bindings.ts b/server/src/services/project-skill-source-bindings.ts new file mode 100644 index 00000000000..a280ae81ad6 --- /dev/null +++ b/server/src/services/project-skill-source-bindings.ts @@ -0,0 +1,64 @@ +import { lockSkillImportQueue, MAX_COMPANY_PENDING_SKILL_IMPORTS, MAX_PROJECT_PENDING_SKILL_IMPORTS, SKILL_IMPORT_CAPACITY_WAIT } from './skill-source-import-queue.js'; +import { and, eq, ne } from 'drizzle-orm'; +import { projectSkillSources, type Db, projectWorkspaces } from '@paperclipai/db'; +import { parseGitHubSkillRepositoryUrl } from '@paperclipai/shared'; +import { currentSkillActor } from './skill-access.js'; +import { captureRunIdentity } from './run-identity.js'; +import { forbidden } from '../errors.js'; + +export async function projectSkillSetupIdentity(db: Db, companyId: string, actor = currentSkillActor()) { + let userId = actor?.type === 'board' ? actor.userId ?? null : null; + let connectionId: string | null = null, refreshGrantId: string | null = null; + if (actor?.type === 'agent' && actor.source === 'agent_jwt') { + if (!actor.runId || !actor.agentId || actor.companyId !== companyId) throw forbidden('Project skill setup requires an authenticated agent run.'); + const identity = await captureRunIdentity(db, { companyId, agentId: actor.agentId, runId: actor.runId }); + userId = identity.run.responsibleUserId; + // Only reuse the connection already authorized by managed GitHub delivery + // for this active run. Responsibility alone cannot discover another grant. + if (identity.context?.github?.status === 'available') { + connectionId = identity.context.github.connectionId ?? null; + refreshGrantId = identity.context.github.grantId ?? null; + } + } + // An API key can attach repositories, but cannot supply a setup person. + // Keep the durable job so an authorized person can adopt it from Sources. + return { refreshUserId: userId, refreshAgentId: actor?.type === 'agent' ? actor.agentId ?? null : null, + refreshLocalTrusted: actor?.source === 'local_implicit' || userId === 'local-board', connectionId, refreshGrantId }; +} + +export function projectSkillRepository(workspace: Pick) { + const url = workspace.repoUrl?.replace(/^git@github\.com:/i, 'https://github.com/'); + const parsed = url ? parseGitHubSkillRepositoryUrl(url) : null; + if (!parsed) return null; + return { repositoryUrl: parsed.repositoryUrl.toLowerCase(), trackingRef: workspace.defaultRef ?? workspace.repoRef ?? parsed.trackingRef ?? 'HEAD' }; +} + +/** Called inside the workspace mutation transaction; no provider calls here. */ +export async function enqueueProjectSkillSource(db: Db, workspace: typeof projectWorkspaces.$inferSelect, setupIdentity?: Awaited>) { + return db.transaction(async tx => { + const db = tx as unknown as Db; + const repository = projectSkillRepository(workspace); + const setup = repository ? setupIdentity ?? await projectSkillSetupIdentity(db, workspace.companyId) : undefined; + const pending = await lockSkillImportQueue(db, workspace.companyId); + await db.update(projectSkillSources).set({ active: false, lastError: null }) + .where(and(eq(projectSkillSources.companyId, workspace.companyId), eq(projectSkillSources.workspaceId, workspace.id), + ...(repository ? [ne(projectSkillSources.trackingRef, repository.trackingRef)] : []))); + if (!repository) return null; + await db.update(projectSkillSources).set({ active: false, lastError: null }) + .where(and(eq(projectSkillSources.companyId, workspace.companyId), eq(projectSkillSources.workspaceId, workspace.id), ne(projectSkillSources.repositoryUrl, repository.repositoryUrl))); + const [existing] = await db.select().from(projectSkillSources).where(and(eq(projectSkillSources.workspaceId, workspace.id), + eq(projectSkillSources.repositoryUrl, repository.repositoryUrl), eq(projectSkillSources.trackingRef, repository.trackingRef))); + const available = Boolean(existing?.sourceId || pending.some(row => row.id === existing?.id)) + || (pending.length < MAX_COMPANY_PENDING_SKILL_IMPORTS && pending.filter(row => row.projectId === workspace.projectId).length < MAX_PROJECT_PENDING_SKILL_IMPORTS); + const [binding] = await db.insert(projectSkillSources).values({ + companyId: workspace.companyId, projectId: workspace.projectId, workspaceId: workspace.id, ...repository, nextSyncAt: new Date(), + ...setup, active: available, lastError: available ? null : SKILL_IMPORT_CAPACITY_WAIT, + }).onConflictDoUpdate({ target: [projectSkillSources.workspaceId, projectSkillSources.repositoryUrl, projectSkillSources.trackingRef], + set: { active: available, lastError: available ? existing?.lastError === SKILL_IMPORT_CAPACITY_WAIT ? null : existing?.lastError : SKILL_IMPORT_CAPACITY_WAIT } }).returning(); + return binding!; + }); +} + +export async function detachProjectSkillSource(db: Db, companyId: string, workspaceId: string) { + await db.update(projectSkillSources).set({ active: false, lastError: null }).where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.workspaceId, workspaceId))); +} diff --git a/server/src/services/projects.ts b/server/src/services/projects.ts index 67c320f530f..30785b5b019 100644 --- a/server/src/services/projects.ts +++ b/server/src/services/projects.ts @@ -1,3 +1,4 @@ +import { enqueueProjectSkillSource, detachProjectSkillSource } from "./project-skill-source-bindings.js"; import { and, asc, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { @@ -41,7 +42,7 @@ import { lockExecutionWorkspaceRootProvenance, preserveConfiguredProjectWorkspac type ProjectTransaction = Parameters[0]>[0]; -async function lockProjectRootMutation(tx: ProjectTransaction, projectId: string) { +export async function lockProjectRootMutation(tx: ProjectTransaction, projectId: string) { const [identity] = await tx.select({ companyId: projects.companyId }).from(projects).where(eq(projects.id, projectId)); if (!identity) return null; await lockExecutionWorkspaceRootProvenance(tx, identity.companyId); @@ -1174,6 +1175,7 @@ export function projectService(db: Db, options: { captureWorkspaceUpdates?: bool }) .returning() .then((rows) => rows[0] ?? null); + if (row) await enqueueProjectSkillSource(tx as unknown as Db, row); return row; }); @@ -1285,6 +1287,9 @@ export function projectService(db: Db, options: { captureWorkspaceUpdates?: bool .returning() .then((rows) => rows[0] ?? null); if (!row) return null; + if (row.repoUrl !== existing.repoUrl || row.defaultRef !== existing.defaultRef || row.repoRef !== existing.repoRef) { + await enqueueProjectSkillSource(tx as unknown as Db, row); + } if (row.isPrimary) return row; @@ -1364,6 +1369,7 @@ export function projectService(db: Db, options: { captureWorkspaceUpdates?: bool await preserveConfiguredProjectWorkspaceRootProvenance(tx, existing); const removed = await db.transaction(async (tx) => { + await detachProjectSkillSource(tx as unknown as Db, existing.companyId, workspaceId); const row = await tx .delete(projectWorkspaces) .where(eq(projectWorkspaces.id, workspaceId)) diff --git a/server/src/services/skill-access.ts b/server/src/services/skill-access.ts new file mode 100644 index 00000000000..e58d2cdd8da --- /dev/null +++ b/server/src/services/skill-access.ts @@ -0,0 +1,99 @@ +import { randomUUID } from 'node:crypto'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { RequestHandler } from 'express'; +import { and, eq, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm'; +import { activityLog, companySkills, companySkillSources, projectSkillSources, projects, type Db } from '@paperclipai/db'; +import { authorizationService, projectReadSqlCondition, type AuthorizationActor } from './authorization.js'; +import { notFound } from '../errors.js'; + +// Carries the authenticated identity through shared services, including native API dispatch. +// Background operations must install their stored, revalidated setup identity explicitly. +const actors = new AsyncLocalStorage(); +export const currentSkillActor = () => actors.getStore(); +export function withSkillActor(actor: AuthorizationActor, operation: () => T): T { + return actors.run(actor, operation); +} +export const skillAccessRequestContext: RequestHandler = (req, _res, next) => withSkillActor(req.actor, next); + +export function skillAccessService(db: Db, actor = currentSkillActor()) { + function canReadCompany(companyId: string) { + return actor?.type !== 'none' && !(actor?.type === 'agent' && actor.companyId !== companyId) + && !(actor?.type === 'board' && actor.companyIds && !actor.companyIds.includes(companyId) + && actor.source !== 'local_implicit' && !actor.isInstanceAdmin); + } + async function projectIds(companyId: string, sourceId?: string) { + const bindings = await db.select({ sourceId: projectSkillSources.sourceId, projectId: projectSkillSources.projectId }) + .from(projectSkillSources).where(and(eq(projectSkillSources.companyId, companyId), isNotNull(projectSkillSources.sourceId), + ...(sourceId ? [eq(projectSkillSources.sourceId, sourceId)] : []))); + if (!actor) return bindings; + const access = authorizationService(db); + const decisions = new Map(); + for (const id of new Set(bindings.map(binding => binding.projectId))) { + decisions.set(id, (await access.decide({ actor, action: 'project:read', + resource: { type: 'project', companyId, projectId: id } })).allowed); + } + return bindings.filter(binding => decisions.get(binding.projectId)); + } + async function visibleSourceIds(companyId: string) { + if (!canReadCompany(companyId)) return []; + if (!actor) return null; + const sources = await db.select({ id: companySkillSources.id, scope: companySkillSources.accessScope }) + .from(companySkillSources).where(eq(companySkillSources.companyId, companyId)); + const permitted = new Set((await projectIds(companyId)).map(binding => binding.sourceId)); + return sources.filter(source => source.scope === 'company' || permitted.has(source.id)).map(source => source.id); + } + async function condition(companyId: string) { + if (!canReadCompany(companyId)) return sql`false`; + const ids = await visibleSourceIds(companyId); + if (ids === null) return sql`true`; + return or(isNull(companySkills.accessSourceId), ids.length ? inArray(companySkills.accessSourceId, ids) : sql`false`)!; + } + async function assertSkill(companyId: string, skillId: string) { + const [row] = await db.select({ id: companySkills.id }).from(companySkills) + .where(and(eq(companySkills.companyId, companyId), eq(companySkills.id, skillId), await condition(companyId))); + if (!row) throw notFound('Skill not found'); + } + async function assertSource(companyId: string, sourceId: string) { + const ids = await visibleSourceIds(companyId); + if (ids !== null && !ids.includes(sourceId)) throw notFound('Skill source not found'); + } + async function activityCondition(companyId: string) { + if (!actor) return sql`true`; + const ids = await visibleSourceIds(companyId); + const skillCondition = await condition(companyId); + const projectCondition = await projectReadSqlCondition(db, actor); + return sql`( + (${activityLog.entityType} != 'company_skill' or exists ( + select 1 from ${companySkills} where ${companySkills.companyId} = ${companyId} + and ${companySkills.id}::text = ${activityLog.entityId} and ${skillCondition})) + and (${activityLog.entityType} != 'company_skill_source' or ${ids?.length ? inArray(activityLog.entityId, ids) : sql`false`}) + and (${activityLog.entityType} != 'project' or exists ( + select 1 from ${projects} where ${projects.companyId} = ${companyId} + and ${projects.id}::text = ${activityLog.entityId} and ${projectCondition})))`; + } + return { canReadCompany, condition, projectIds, visibleSourceIds, assertSkill, assertSource, activityCondition }; +} + +/** Independent editable forks retain the originating project set at creation. + * Membership/visibility remain dynamic; later repository attachments grant no access. + */ +export async function retainProjectSkillAccess(db: Db, companyId: string, sourceId: string | null | undefined) { + if (!sourceId) return null; + return db.transaction(async tx => { + const [source] = await tx.select().from(companySkillSources).where(and( + eq(companySkillSources.companyId, companyId), eq(companySkillSources.id, sourceId))); + if (!source || source.removedAt || source.accessScope !== 'projects') return sourceId; + const origins = await tx.select({ projectId: projectSkillSources.projectId }).from(projectSkillSources) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.sourceId, sourceId))); + const id = randomUUID(); + await tx.insert(companySkillSources).values({ id, companyId, repositoryId: source.repositoryId, + repositoryUrl: source.repositoryUrl, fullName: source.fullName, trackingRef: source.trackingRef, + accessScope: 'projects', removedAt: new Date(), enabled: false, automaticUpdates: false }); + const projectIds = [...new Set(origins.map(origin => origin.projectId))]; + if (projectIds.length) await tx.insert(projectSkillSources).values(projectIds.map(projectId => ({ + companyId, projectId, sourceId: id, repositoryUrl: source.repositoryUrl, trackingRef: source.trackingRef, + active: false, status: 'ready' as const, + }))); + return id; + }); +} diff --git a/server/src/services/skill-source-git-snapshot.ts b/server/src/services/skill-source-git-snapshot.ts index 312f1519adb..8f53a4fe3b2 100644 --- a/server/src/services/skill-source-git-snapshot.ts +++ b/server/src/services/skill-source-git-snapshot.ts @@ -11,6 +11,8 @@ import { MAX_REPOSITORY_BYTES, MAX_REPOSITORY_FILES } from './skill-repository-p export type GitSkillTreeEntry = { path: string; type: string; mode: string; sha: string; size?: number }; export interface GitSkillSnapshot { commitSha: string; + treeSha?: string; + downloadBytes?: number; entries: GitSkillTreeEntry[]; readBlob: (entry: GitSkillTreeEntry, signal?: AbortSignal) => Promise; readBlobs?: (entries: GitSkillTreeEntry[], signal?: AbortSignal) => Promise>; @@ -26,7 +28,7 @@ const MAX_DOWNLOAD_BYTES = 128 * 1024 * 1024; const MAX_TREE_BYTES = 32 * 1024 * 1024; const CACHE_TTL_MS = 10 * 60_000; const MAX_CACHED_SNAPSHOTS = 4; -const snapshots = new Map(); +const snapshots = new Map(); let activeDownloads = 0; /** Only structured numeric progress crosses the process boundary; never forward stderr. */ @@ -180,11 +182,11 @@ async function discard(key: string) { await rm(cached.directory, { recursive: true, force: true }); } -function acquire(cached: NonNullable>, key: string): GitSkillSnapshot { +function acquire(cached: NonNullable>, key: string, downloadBytes = 0): GitSkillSnapshot { cached.users++; clearTimeout(cached.timer); let released = false; - return { commitSha: cached.commitSha, entries: cached.entries, + return { commitSha: cached.commitSha, treeSha: cached.treeSha, downloadBytes, entries: cached.entries, readBlob: async (entry, signal) => { if (released || !/^[a-f0-9]{40}$/.test(entry.sha) || entry.type !== 'blob' || (entry.size ?? Infinity) > 1024 * 1024) throw unprocessable('Invalid snapshot file.'); return runGit(cached.directory, ['cat-file', 'blob', entry.sha], '', { signal, maxOutput: 1024 * 1024 }); @@ -253,17 +255,20 @@ export async function openGitSkillSnapshot(input: { repositoryUrl: string; ref: if (!input.commitSha && !/^[a-f0-9]{40}$/i.test(input.ref)) await runGit(directory, ['check-ref-format', '--branch', input.ref], '', options); await runGit(directory, ['-c', 'fetch.unpackLimit=1', 'fetch', '--depth=1', '--no-tags', '--no-recurse-submodules', '--no-auto-maintenance', '--progress', '--', `${parsed.repositoryUrl}.git`, input.commitSha ?? input.ref], input.token, { ...options, download: true }); - if (await directoryBytes(directory) > MAX_DOWNLOAD_BYTES) throw unprocessable('Repository download exceeds the 128 MB limit. Use a smaller skills repository.'); + const downloadBytes = await directoryBytes(directory); + if (downloadBytes > MAX_DOWNLOAD_BYTES) throw unprocessable('Repository download exceeds the 128 MB limit. Use a smaller skills repository.'); const commitSha = (await runGit(directory, ['rev-parse', '--verify', 'FETCH_HEAD^{commit}'], '', options)).toString('utf8').trim(); if (!/^[a-f0-9]{40}$/.test(commitSha) || (input.commitSha && commitSha !== input.commitSha.toLowerCase())) throw unprocessable('The downloaded commit did not match the requested snapshot.'); + const treeSha = (await runGit(directory, ['rev-parse', '--verify', `${commitSha}^{tree}`], '', options)).toString('utf8').trim(); + if (!/^[a-f0-9]{40}$/.test(treeSha)) throw unprocessable('Git did not return an immutable tree.'); const entries = parseTree(await runGit(directory, ['ls-tree', '-r', '-l', '-z', '--full-tree', commitSha], '', options)); const key = `${prefix}${commitSha}`; const duplicate = snapshots.get(key); - if (duplicate) return acquire(duplicate, key); - const cached = { directory, commitSha, entries, users: 0, expires: Date.now() + CACHE_TTL_MS }; + if (duplicate) return acquire(duplicate, key, downloadBytes); + const cached = { directory, commitSha, treeSha, entries, users: 0, expires: Date.now() + CACHE_TTL_MS }; snapshots.set(key, cached); directory = undefined; - return acquire(cached, key); + return acquire(cached, key, downloadBytes); } finally { activeDownloads--; if (directory) await rm(directory, { recursive: true, force: true }); diff --git a/server/src/services/skill-source-github-access.ts b/server/src/services/skill-source-github-access.ts index 2b3f0ec9d79..1388245b7ea 100644 --- a/server/src/services/skill-source-github-access.ts +++ b/server/src/services/skill-source-github-access.ts @@ -15,7 +15,7 @@ function providerStatus(error: unknown): number | undefined { } /** Automatic access considers only this caller's grants, never connection management visibility. */ -export function skillSourceGitHubReader(db: Db, companyId: string, actor: Request['actor'], connectionId: string | null): GitHubRead { +export function skillSourceGitHubReader(db: Db, companyId: string, actor: Request['actor'], connectionId: string | null, options: { grantId?: string | null } = {}): GitHubRead { let chosen: Authorization | undefined; const access = toolAccessService(db); const localTrusted = actor.type === 'board' && actor.source === 'local_implicit'; @@ -44,7 +44,10 @@ export function skillSourceGitHubReader(db: Db, companyId: string, actor: Reques const ids = connectionId ? [connectionId] : await access.githubReadConnectionIds(companyId, actor.userId ?? null, localTrusted); const result: Authorization[] = []; for (const id of ids) { - for (const grantId of await access.githubReadGrantIds(companyId, id, actor.userId ?? null, localTrusted)) result.push({ connectionId: id, grantId }); + for (const grantId of await access.githubReadGrantIds(companyId, id, actor.userId ?? null, localTrusted)) { + if (options.grantId && options.grantId !== grantId) continue; + result.push({ connectionId: id, grantId }); + } } result.sort((a, b) => Number(b.connectionId === chosen?.connectionId && b.grantId === chosen?.grantId) - Number(a.connectionId === chosen?.connectionId && a.grantId === chosen?.grantId)); if (!connectionId) result.push({ connectionId: null }); @@ -100,7 +103,13 @@ export function skillSourceGitHubReader(db: Db, companyId: string, actor: Reques : response.status === 404 ? 'Repository, branch, or file is unavailable. Check the URL and GitHub repository access.' : [401, 403].includes(response.status) ? 'GitHub denied repository access. Reconnect GitHub or choose an authorized connection.' : 'GitHub could not complete the read. Try again.'; - throw unprocessable(message, { code: limited ? 'skill_source_github_rate_limited' : 'skill_source_github_read_failed', status: response.status }); + const retryAfter = response.headers.get('retry-after'); + const resetAt = Number(response.headers.get('x-ratelimit-reset')) * 1_000; + const retryAfterMs = retryAfter && /^\d+$/.test(retryAfter) + ? Number(retryAfter) * 1_000 + : retryAfter ? Date.parse(retryAfter) - Date.now() : resetAt - Date.now(); + throw unprocessable(message, { code: limited ? 'skill_source_github_rate_limited' : 'skill_source_github_read_failed', status: response.status, + ...(limited && Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? { retryAfterMs } : {}) }); } return response.json(); }, signal); @@ -129,6 +138,7 @@ export function skillSourceGitHubReader(db: Db, companyId: string, actor: Reques throw error; } }; + Object.defineProperty(read, 'grantId', { get: () => chosen?.grantId ?? null }); Object.defineProperty(read, 'connectionId', { get: () => chosen?.connectionId ?? null }); return read; } diff --git a/server/src/services/skill-source-import-budget.ts b/server/src/services/skill-source-import-budget.ts new file mode 100644 index 00000000000..95969ed03a2 --- /dev/null +++ b/server/src/services/skill-source-import-budget.ts @@ -0,0 +1,32 @@ +import { and, eq, isNull, sql } from 'drizzle-orm'; +import { companySkillImportBudgets, projectSkillSources, type Db } from '@paperclipai/db'; +import { conflict, tooManyRequests } from '../errors.js'; +import { automaticSkillWorkAvailable, automaticSkillWorkRefund, AUTOMATIC_SKILL_WORK_CAPACITY_BYTES, + AUTOMATIC_SKILL_WORK_RESERVATION_BYTES, REFILL_WINDOW_MS } from './skill-source-refresh-budget.js'; + +/** Reserve before provider work; failed attempts retain their reservation. */ +export async function reserveInitialSkillWork(db: Db, binding: typeof projectSkillSources.$inferSelect, token: string, now: Date) { + await db.transaction(async tx => { + const [current] = await tx.select({ id: projectSkillSources.id }).from(projectSkillSources).where(and( + eq(projectSkillSources.companyId, binding.companyId), eq(projectSkillSources.id, binding.id), + eq(projectSkillSources.leaseToken, token), eq(projectSkillSources.active, true), isNull(projectSkillSources.sourceId))).for('no key update'); + if (!current) throw conflict('Repository import changed. Retry from Skills.'); + await tx.insert(companySkillImportBudgets).values({ companyId: binding.companyId, updatedAt: now }).onConflictDoNothing(); + const [budget] = await tx.select().from(companySkillImportBudgets).where(eq(companySkillImportBudgets.companyId, binding.companyId)).for('update'); + const effectiveNow = new Date(Math.max(now.getTime(), budget!.updatedAt.getTime())); + const available = automaticSkillWorkAvailable(budget!.availableBytes, budget!.updatedAt, effectiveNow); + if (available < AUTOMATIC_SKILL_WORK_RESERVATION_BYTES) throw tooManyRequests('Automatic repository import is temporarily limited. It will resume when work credits refill.', { + code: 'skill_source_initial_work_limited', + retryAfterMs: Math.ceil((AUTOMATIC_SKILL_WORK_RESERVATION_BYTES - available) * REFILL_WINDOW_MS / AUTOMATIC_SKILL_WORK_CAPACITY_BYTES), + }); + await tx.update(companySkillImportBudgets).set({ availableBytes: available - AUTOMATIC_SKILL_WORK_RESERVATION_BYTES, updatedAt: effectiveNow }) + .where(eq(companySkillImportBudgets.companyId, binding.companyId)); + }); +} + +/** Called once, inside the transaction that successfully links the binding. */ +export async function settleInitialSkillWork(db: Db, companyId: string, work: { downloadBytes?: number; scanBytes?: number }) { + const refund = automaticSkillWorkRefund(work); + if (refund) await db.update(companySkillImportBudgets).set({ availableBytes: sql`least(${AUTOMATIC_SKILL_WORK_CAPACITY_BYTES}, ${companySkillImportBudgets.availableBytes} + ${refund})` }) + .where(eq(companySkillImportBudgets.companyId, companyId)); +} diff --git a/server/src/services/skill-source-import-queue.ts b/server/src/services/skill-source-import-queue.ts new file mode 100644 index 00000000000..704e213cd7b --- /dev/null +++ b/server/src/services/skill-source-import-queue.ts @@ -0,0 +1,44 @@ +import { and, asc, eq, isNull, isNotNull, notInArray } from 'drizzle-orm'; +import { companies, projectSkillSources, projectWorkspaces, type Db } from '@paperclipai/db'; + +export const MAX_COMPANY_PENDING_SKILL_IMPORTS = 100; +export const MAX_PROJECT_PENDING_SKILL_IMPORTS = 20; +export const SKILL_IMPORT_CAPACITY_WAIT = 'Waiting for earlier repository imports to finish.'; + +/** Call inside the attachment transaction, before locking any binding rows. */ +export async function lockSkillImportQueue(db: Db, companyId: string) { + await db.select({ id: companies.id }).from(companies).where(eq(companies.id, companyId)).for('no key update'); + return db.select({ projectId: projectSkillSources.projectId, id: projectSkillSources.id }).from(projectSkillSources) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.active, true), isNull(projectSkillSources.sourceId))) + .limit(MAX_COMPANY_PENDING_SKILL_IMPORTS); +} + +/** Deferred provenance stays durable; free slots admit it without user ceremony. */ +export async function admitDeferredSkillImports(db: Db, companyId: string, now: Date) { + return db.transaction(async tx => { + const pending = await lockSkillImportQueue(tx as unknown as Db, companyId); + let count = pending.length; + const perProject = new Map(); + for (const row of pending) perProject.set(row.projectId, (perProject.get(row.projectId) ?? 0) + 1); + if (count >= MAX_COMPANY_PENDING_SKILL_IMPORTS) return; + const fullProjects = [...perProject].filter(([, count]) => count >= MAX_PROJECT_PENDING_SKILL_IMPORTS).map(([id]) => id); + const waiting = await tx.select({ id: projectSkillSources.id, projectId: projectSkillSources.projectId }) + .from(projectSkillSources).innerJoin(projectWorkspaces, eq(projectWorkspaces.id, projectSkillSources.workspaceId)) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.active, false), + isNull(projectSkillSources.sourceId), eq(projectSkillSources.status, 'pending'), + eq(projectSkillSources.lastError, SKILL_IMPORT_CAPACITY_WAIT), + ...(fullProjects.length ? [notInArray(projectSkillSources.projectId, fullProjects)] : []))) + .orderBy(asc(projectSkillSources.createdAt)).limit(MAX_COMPANY_PENDING_SKILL_IMPORTS); + for (const row of waiting) { + if (count >= MAX_COMPANY_PENDING_SKILL_IMPORTS) break; + if ((perProject.get(row.projectId) ?? 0) >= MAX_PROJECT_PENDING_SKILL_IMPORTS) continue; + await tx.update(projectSkillSources).set({ active: true, lastError: null, nextSyncAt: now }) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.id, row.id), + isNotNull(projectSkillSources.workspaceId), eq(projectSkillSources.active, false), + isNull(projectSkillSources.sourceId), eq(projectSkillSources.status, 'pending'), + eq(projectSkillSources.lastError, SKILL_IMPORT_CAPACITY_WAIT))); + count++; + perProject.set(row.projectId, (perProject.get(row.projectId) ?? 0) + 1); + } + }); +} diff --git a/server/src/services/skill-source-refresh-budget.ts b/server/src/services/skill-source-refresh-budget.ts new file mode 100644 index 00000000000..e13a4c1439f --- /dev/null +++ b/server/src/services/skill-source-refresh-budget.ts @@ -0,0 +1,43 @@ +import { and, eq, sql } from 'drizzle-orm'; +import { companySkillSources, type Db } from '@paperclipai/db'; +import { conflict, tooManyRequests } from '../errors.js'; + +export const AUTOMATIC_SKILL_WORK_CAPACITY_BYTES = 512 * 1024 * 1024; +export const AUTOMATIC_SKILL_WORK_RESERVATION_BYTES = 128 * 1024 * 1024; +export const REFILL_WINDOW_MS = 60 * 60_000; + +export function automaticSkillWorkAvailable(bytes: number, updatedAt: Date, now: Date) { + const elapsed = Math.max(0, now.getTime() - updatedAt.getTime()); + return Math.min(AUTOMATIC_SKILL_WORK_CAPACITY_BYTES, + bytes + Math.floor(Math.min(elapsed, REFILL_WINDOW_MS) * AUTOMATIC_SKILL_WORK_CAPACITY_BYTES / REFILL_WINDOW_MS)); +} +export function automaticSkillWorkRefund(work: { downloadBytes?: number; scanBytes?: number }) { + return AUTOMATIC_SKILL_WORK_RESERVATION_BYTES - Math.ceil(Math.min(AUTOMATIC_SKILL_WORK_RESERVATION_BYTES, + Math.max(work.downloadBytes ?? AUTOMATIC_SKILL_WORK_RESERVATION_BYTES, work.scanBytes ?? AUTOMATIC_SKILL_WORK_RESERVATION_BYTES, 0))); +} + +/** The refresh lease serializes this durable token bucket across workers/restarts. */ +export async function reserveAutomaticSkillWork(db: Db, source: typeof companySkillSources.$inferSelect, token: string, now: Date) { + const effectiveNow = new Date(Math.max(now.getTime(), source.automaticBudgetAt.getTime())); + const available = automaticSkillWorkAvailable(source.automaticBudgetBytes, source.automaticBudgetAt, effectiveNow); + if (available < AUTOMATIC_SKILL_WORK_RESERVATION_BYTES) { + throw tooManyRequests('Automatic refresh is temporarily limited. Use Refresh now, or wait for automatic updates to resume.', { + code: 'skill_source_automatic_work_limited', + retryAfterSeconds: Math.ceil((AUTOMATIC_SKILL_WORK_RESERVATION_BYTES - available) * REFILL_WINDOW_MS / AUTOMATIC_SKILL_WORK_CAPACITY_BYTES / 1000), + }); + } + const changed = await db.update(companySkillSources).set({ automaticBudgetAt: effectiveNow, + automaticBudgetBytes: available - AUTOMATIC_SKILL_WORK_RESERVATION_BYTES }) + .where(and(eq(companySkillSources.companyId, source.companyId), eq(companySkillSources.id, source.id), + eq(companySkillSources.leaseToken, token))).returning({ id: companySkillSources.id }); + if (!changed.length) throw conflict('Source refresh changed. Try again.'); +} + +/** Small repositories refund unused capacity; failures keep the bounded reservation. */ +export async function settleAutomaticSkillWork(db: Db, companyId: string, sourceId: string, token: string, + work: { downloadBytes?: number; scanBytes?: number }) { + const refund = automaticSkillWorkRefund(work); + if (!refund) return; + await db.update(companySkillSources).set({ automaticBudgetBytes: sql`least(${AUTOMATIC_SKILL_WORK_CAPACITY_BYTES}, ${companySkillSources.automaticBudgetBytes} + ${refund})` }) + .where(and(eq(companySkillSources.companyId, companyId), eq(companySkillSources.id, sourceId), eq(companySkillSources.leaseToken, token))); +} diff --git a/server/src/services/skill-source-retry.ts b/server/src/services/skill-source-retry.ts new file mode 100644 index 00000000000..9b1ba3f09a3 --- /dev/null +++ b/server/src/services/skill-source-retry.ts @@ -0,0 +1,9 @@ +/** Share retry scheduling between initial imports and subsequent refreshes. */ +export function skillSourceRetryAt(error: unknown, failureCount: number, now = new Date()) { + const backoff = Math.min(60 * 60_000, 5 * 60_000 * 2 ** Math.min(failureCount, 4)); + const providerDelay = error instanceof Error && 'details' in error + ? (error.details as { retryAfterMs?: unknown } | null)?.retryAfterMs : undefined; + const delay = typeof providerDelay === 'number' && Number.isFinite(providerDelay) + ? Math.max(backoff, Math.min(24 * 60 * 60_000, providerDelay)) : backoff; + return new Date(now.getTime() + delay); +} diff --git a/server/src/services/skill-source-sync.ts b/server/src/services/skill-source-sync.ts new file mode 100644 index 00000000000..3cb094deaeb --- /dev/null +++ b/server/src/services/skill-source-sync.ts @@ -0,0 +1,314 @@ +import { lockProjectRootMutation } from './projects.js'; +import { reserveInitialSkillWork, settleInitialSkillWork } from './skill-source-import-budget.js'; +import { admitDeferredSkillImports, lockSkillImportQueue, SKILL_IMPORT_CAPACITY_WAIT } from './skill-source-import-queue.js'; +import { randomUUID } from 'node:crypto'; +import { skillSourceRetryAt } from './skill-source-retry.js'; +import { and, asc, eq, inArray, isNotNull, isNull, lte, or, sql } from 'drizzle-orm'; +import { activityLog, agents, authUsers, heartbeatRuns, companies, companyMemberships, companySkillSources, companySkillSourceEntries, + projectSkillSources, projectWorkspaces, projects, type Db } from '@paperclipai/db'; +import type { ProjectSkillRepository } from '@paperclipai/shared'; +import { authorizationService, type AuthorizationActor } from './authorization.js'; +import { skillSourceGitHubReader } from './skill-source-github-access.js'; +import { skillSourceService, type SkillSourceContext } from './skill-sources.js'; +import { scanGitHubSkillsWithPackageFallback } from './github-skill-source.js'; +import { companySkillPolicyService } from './company-skill-policy.js'; +import { persistActivity, publishActivity } from './activity-log.js'; +import type { ActivityPublication } from './activity-log.js'; +import { currentSkillActor, skillAccessService } from './skill-access.js'; +import { enqueueProjectSkillSource, projectSkillRepository, projectSkillSetupIdentity } from './project-skill-source-bindings.js'; +import { conflict, forbidden, notFound } from '../errors.js'; +import { loadConfig } from '../config.js'; + +export const SKILL_SOURCE_REFRESH_MS = 5 * 60_000; +type Setup = { companyId: string; refreshUserId: string | null; refreshAgentId: string | null; + refreshLocalTrusted: boolean; refreshGrantId: string | null; connectionId: string | null }; + +export async function projectSkillRepositories(db: Db, companyId: string, actor: AuthorizationActor): Promise { + const rows = await db.select({ workspace: projectWorkspaces, project: projects }).from(projectWorkspaces) + .innerJoin(projects, and(eq(projects.id, projectWorkspaces.projectId), eq(projects.companyId, companyId))) + .where(eq(projectWorkspaces.companyId, companyId)).orderBy(asc(projects.name), asc(projectWorkspaces.name)); + const bindings = await db.select().from(projectSkillSources).where(eq(projectSkillSources.companyId, companyId)); + const removed = new Set((await db.select({ id: companySkillSources.id }).from(companySkillSources) + .where(and(eq(companySkillSources.companyId, companyId), sql`${companySkillSources.removedAt} is not null`))).map(source => source.id)); + const allowed = new Map(); + const access = authorizationService(db); + const result: ProjectSkillRepository[] = []; + for (const { workspace, project } of rows) { + const repo = projectSkillRepository(workspace); + if (!repo) continue; + if (!allowed.has(project.id)) allowed.set(project.id, (await access.decide({ actor, action: 'project:read', + resource: { type: 'project', companyId, projectId: project.id } })).allowed); + if (!allowed.get(project.id)) continue; + const binding = bindings.find(item => (item.active || (!item.sourceId && item.lastError !== null)) && item.workspaceId === workspace.id && item.repositoryUrl === repo.repositoryUrl && item.trackingRef === repo.trackingRef); + result.push({ projectId: project.id, projectName: project.name, projectVisibility: project.visibility as 'open' | 'private', + workspaceId: workspace.id, ...repo, bindingId: binding?.id ?? null, sourceId: binding?.sourceId && !removed.has(binding.sourceId) ? binding.sourceId : null, + status: binding?.sourceId && removed.has(binding.sourceId) ? 'available' : binding?.status ?? 'available', lastError: binding?.lastError ?? null }); + } + return result; +} + +export async function adoptProjectSkillRepository(db: Db, companyId: string, workspaceId: string, authorize: SkillSourceContext['authorize']) { + const actor = currentSkillActor(); + if (!actor) throw forbidden('Authentication required'); + const [workspace] = await db.select().from(projectWorkspaces).where(and(eq(projectWorkspaces.companyId, companyId), eq(projectWorkspaces.id, workspaceId))); + if (!workspace || !(await authorizationService(db).decide({ actor, action: 'project:read', resource: { type: 'project', companyId, projectId: workspace.projectId } })).allowed) throw notFound('Project repository not found'); + const repository = projectSkillRepository(workspace); + if (!repository) throw notFound('Project repository not found'); + await authorize('skills.import', { sourceType: 'git', sourceLocator: repository.repositoryUrl }); + return db.transaction(async tx => { + const database = tx as unknown as Db; + // Follow the editor: root provenance, project, workspace, then queue/binding. + const project = await lockProjectRootMutation(tx, workspace.projectId); + if (!project || project.companyId !== companyId) throw notFound('Project repository not found'); + const [currentWorkspace] = await tx.select().from(projectWorkspaces).where(and( + eq(projectWorkspaces.companyId, companyId), eq(projectWorkspaces.projectId, project.id), eq(projectWorkspaces.id, workspaceId))).for('update'); + if (!currentWorkspace) throw notFound('Project repository not found'); + const currentRepository = projectSkillRepository(currentWorkspace); + if (!currentRepository || currentRepository.repositoryUrl !== repository.repositoryUrl || currentRepository.trackingRef !== repository.trackingRef) { + throw conflict('Project repository changed. Reload Sources and try again.'); + } + if (!(await authorizationService(database).decide({ actor, action: 'project:read', resource: { type: 'project', companyId, projectId: project.id } })).allowed) throw notFound('Project repository not found'); + // Capture the setup before the company queue, consistently with attachment. + const setup = await projectSkillSetupIdentity(database, companyId); + await lockSkillImportQueue(database, companyId); + const [previous] = await tx.select({ bindingId: projectSkillSources.id, removedAt: companySkillSources.removedAt }) + .from(projectSkillSources).innerJoin(companySkillSources, eq(companySkillSources.id, projectSkillSources.sourceId)) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.workspaceId, workspace.id), + eq(projectSkillSources.repositoryUrl, repository.repositoryUrl), eq(projectSkillSources.trackingRef, repository.trackingRef))) + .for('update'); + if (previous?.removedAt) { + // Retain the original project's access provenance, but free this workspace + // for a new tracking identity. Never attach new readers to retained copies. + await tx.update(projectSkillSources).set({ workspaceId: null, active: false }) + .where(eq(projectSkillSources.id, previous.bindingId)); + } + const binding = await enqueueProjectSkillSource(database, currentWorkspace, setup); + if (!binding) throw notFound('Project repository not found'); + if (!binding.sourceId) await tx.update(projectSkillSources).set({ status: 'pending', nextSyncAt: new Date(), failureCount: 0, lastError: binding.lastError === SKILL_IMPORT_CAPACITY_WAIT ? SKILL_IMPORT_CAPACITY_WAIT : null, + ...setup, + }).where(eq(projectSkillSources.id, binding.id)); + const { publication } = await persistActivity(database, { companyId, actorType: actor.type === 'agent' ? 'agent' : 'user', + actorId: actor.agentId ?? actor.userId ?? 'board', agentId: actor.agentId, runId: actor.runId, + action: 'project.skills_import_requested', entityType: 'project', entityId: workspace.projectId, details: { workspaceId, bindingId: binding.id } }); + // Publish only after the transaction completes (caller's route refreshes query state). + return { bindingId: binding.id, publication }; + }); +} + +function setupActor(setup: Setup): AuthorizationActor { + return setup.refreshAgentId ? { type: 'agent', agentId: setup.refreshAgentId, companyId: setup.companyId, + onBehalfOfUserId: setup.refreshUserId === 'local-board' ? null : setup.refreshUserId, source: 'agent_key' } + : { type: 'board', userId: setup.refreshUserId, source: setup.refreshLocalTrusted ? 'local_implicit' : 'session', companyIds: [setup.companyId] }; +} +async function setupContext(db: Db, setup: Setup): Promise { + if (!setup.refreshUserId && !setup.refreshLocalTrusted) throw forbidden('Choose a GitHub connection to enable automatic updates.'); + if (!setup.refreshLocalTrusted) { + const [member] = await db.select({ id: companyMemberships.id }).from(companyMemberships) + .innerJoin(authUsers, eq(authUsers.id, companyMemberships.principalId)).where(and( + eq(companyMemberships.companyId, setup.companyId), eq(companyMemberships.principalType, 'user'), + eq(companyMemberships.principalId, setup.refreshUserId!), eq(companyMemberships.status, 'active'))); + if (!member) throw forbidden('The person who connected this source no longer has company access. Choose another GitHub connection.'); + } + if (setup.refreshAgentId) { + const [agent] = await db.select({ status: agents.status }).from(agents).where(and(eq(agents.companyId, setup.companyId), eq(agents.id, setup.refreshAgentId))); + if (!agent || agent.status === 'terminated') throw forbidden('The agent who connected this source is unavailable. Choose a GitHub connection.'); + } + if (setup.refreshAgentId && (!setup.connectionId || !setup.refreshGrantId)) throw forbidden('Choose a GitHub connection to authorize this agent-created repository.'); + const readerActor: AuthorizationActor = { type: 'board', userId: setup.refreshUserId, + source: setup.refreshLocalTrusted ? 'local_implicit' : 'session', companyIds: [setup.companyId] }; + const actor = setupActor(setup); + const access = authorizationService(db), policies = companySkillPolicyService(db); + const principal = setup.refreshAgentId ? await policies.resolveAgentPrincipal(setup.companyId, setup.refreshAgentId) + : { type: 'board' as const, id: setup.refreshUserId ?? 'board', role: 'board' }; + const readers = new Map>(); + const publications: ActivityPublication[] = []; + return { + actor: { type: 'system', userId: setup.refreshUserId, agentId: setup.refreshAgentId }, + setup: { userId: setup.refreshUserId, agentId: setup.refreshAgentId, localTrusted: setup.refreshLocalTrusted, grantId: setup.refreshGrantId }, + read: connectionId => { + const key = connectionId ?? ''; + for (const reader of readers.values()) if (reader.connectionId && reader.connectionId === connectionId) return reader; + if (!readers.has(key)) readers.set(key, skillSourceGitHubReader(db, setup.companyId, readerActor as Parameters[2], connectionId, { grantId: setup.refreshGrantId })); + return readers.get(key)!; + }, + authorize: async (action, resource) => { + const platform = await access.decide({ actor, action: 'skill_config:update', resource: { type: 'company', companyId: setup.companyId } }); + if (!platform.allowed && !['deny_no_grant', 'deny_missing_consent', 'deny_missing_grant'].includes(platform.reason)) throw forbidden('Automatic skill updates are unavailable for the setup identity.'); + if (resource.skillId) await skillAccessService(db, actor).assertSkill(setup.companyId, resource.skillId); + const decision = await policies.evaluate({ companyId: setup.companyId, principal, action, resource }); + if (!decision.allowed) throw forbidden('Automatic skill updates are denied by company policy.'); + }, + audit: async (tx, sourceId, action, details) => { + const { publication } = await persistActivity(tx as unknown as Db, { companyId: setup.companyId, actorType: 'system', actorId: 'skill-source-sync', + agentId: setup.refreshAgentId, action, entityType: 'company_skill_source', entityId: sourceId, + details: { ...details, refreshUserId: setup.refreshUserId } }); + publications.push(publication); + }, + publishCommittedActivity: () => { for (const publication of publications.splice(0)) publishActivity(publication); }, + }; +} + +export function skillSourceSyncService(db: Db, options: { context?: (setup: Setup) => Promise } = {}) { + const contextFor = options.context ?? (setup => setupContext(db, setup)); + const sources = skillSourceService(db); + let running: Promise<{ imported: number; checked: number }> | null = null; + const shutdown = new AbortController(); + + async function recoverSetup(source: typeof companySkillSources.$inferSelect) { + if (source.refreshUserId || source.refreshLocalTrusted || source.refreshAgentId) return source; + const installed = await db.select({ id: companySkillSourceEntries.skillId }).from(companySkillSourceEntries) + .where(and(eq(companySkillSourceEntries.companyId, source.companyId), eq(companySkillSourceEntries.sourceId, source.id))); + const skillIds = installed.map(entry => entry.id).filter((id): id is string => Boolean(id)); + const [event] = await db.select().from(activityLog).where(and(eq(activityLog.companyId, source.companyId), + or(eq(activityLog.actorType, 'user'), and(eq(activityLog.actorType, 'agent'), isNotNull(activityLog.runId))), + inArray(activityLog.action, ['company.skill_source_refreshed', 'company.skill_imported', 'company.skills_imported']), + or(eq(activityLog.entityId, source.id), ...(skillIds.length ? [inArray(activityLog.entityId, skillIds)] : [])))) + .orderBy(asc(activityLog.createdAt)).limit(1); + if (!event || !event.actorId || event.actorId === 'board') return source; + let userId = event.actorId; + if (event.actorType === 'agent') { + const [run] = await db.select({ userId: heartbeatRuns.responsibleUserId }).from(heartbeatRuns).where(and( + eq(heartbeatRuns.companyId, source.companyId), eq(heartbeatRuns.id, event.runId!), + eq(heartbeatRuns.agentId, event.agentId ?? event.actorId))); + if (!run?.userId) return source; + userId = run.userId; + } + if (!userId) return source; + const localTrusted = userId === 'local-board' && loadConfig().deploymentMode === 'local_trusted'; + const [updated] = await db.update(companySkillSources).set({ refreshUserId: userId, + refreshAgentId: event.actorType === 'agent' ? event.agentId ?? event.actorId : null, + refreshLocalTrusted: localTrusted, + }).where(and(eq(companySkillSources.id, source.id), isNull(companySkillSources.refreshUserId))).returning(); + return updated ?? (await db.select().from(companySkillSources).where(eq(companySkillSources.id, source.id)))[0]!; + } + + async function importBinding(binding: typeof projectSkillSources.$inferSelect, now: Date) { + const token = randomUUID(); + const [claimed] = await db.update(projectSkillSources).set({ leaseToken: token, leaseExpiresAt: new Date(now.getTime() + 10 * 60_000) }) + .where(and(eq(projectSkillSources.id, binding.id), eq(projectSkillSources.active, true), isNull(projectSkillSources.sourceId), + or(isNull(projectSkillSources.leaseToken), lte(projectSkillSources.leaseExpiresAt, now)))).returning(); + if (!claimed) return false; + try { + if (!options.context) { + const actor = setupActor(binding); + if (!(await authorizationService(db).decide({ actor, action: 'project:read', resource: { type: 'project', companyId: binding.companyId, projectId: binding.projectId } })).allowed) throw forbidden('Project access changed. Choose an authorized GitHub connection to retry.'); + } + const context = await contextFor(binding); + await context.authorize('skills.import', { sourceType: 'git', sourceLocator: binding.repositoryUrl }); + await reserveInitialSkillWork(db, binding, token, now); + const reader = context.read(binding.connectionId); + const scan = await scanGitHubSkillsWithPackageFallback({ repositoryUrl: binding.repositoryUrl, trackingRef: binding.trackingRef, packageMode: 'repository' }, reader, { signal: shutdown.signal }); + const [existing] = await db.select().from(companySkillSources).where(and(eq(companySkillSources.companyId, binding.companyId), + eq(companySkillSources.repositoryUrl, scan.repositoryUrl), eq(companySkillSources.trackingRef, binding.trackingRef), isNull(companySkillSources.removedAt))); + let sourceId: string; + if (existing) { + await context.authorize('skills.import', { sourceType: 'git', sourceLocator: existing.repositoryUrl }); + sourceId = existing.id; + } else { + try { + const result = await sources.create(binding.companyId, { repositoryUrl: scan.repositoryUrl, trackingRef: binding.trackingRef, + commitSha: scan.commitSha, packageMode: scan.packageMode, selectedPaths: scan.candidates.map(candidate => candidate.path), + connectionId: scan.connectionId ?? binding.connectionId }, { ...context, projectBindingId: binding.id, projectBindingLeaseToken: token }, scan); + sourceId = result.source.id; + } catch (error) { + if (!(error instanceof Error && 'status' in error && error.status === 409)) throw error; + const [concurrent] = await db.select().from(companySkillSources).where(and(eq(companySkillSources.companyId, binding.companyId), eq(companySkillSources.repositoryUrl, scan.repositoryUrl), eq(companySkillSources.trackingRef, binding.trackingRef), isNull(companySkillSources.removedAt))); + if (!concurrent) throw error; + sourceId = concurrent.id; + } + } + return await db.transaction(async tx => { + // Removal locks the source before its bindings. Follow the same order + // and recheck provenance while that lock prevents concurrent removal. + const [source] = await tx.select().from(companySkillSources).where(and( + eq(companySkillSources.companyId, binding.companyId), eq(companySkillSources.id, sourceId))).for('update'); + if (!source || source.removedAt) throw conflict('Source was removed during import. Retry to start independent tracking.'); + const linked = await tx.update(projectSkillSources).set({ sourceId, status: 'ready', lastError: null, failureCount: 0, leaseToken: null, leaseExpiresAt: null }) + .where(and(eq(projectSkillSources.id, binding.id), eq(projectSkillSources.companyId, binding.companyId), + eq(projectSkillSources.active, true), eq(projectSkillSources.leaseToken, token))).returning({ id: projectSkillSources.id }); + if (linked.length) await settleInitialSkillWork(tx as unknown as Db, binding.companyId, scan.packageMode === 'skills' ? {} : scan); + return linked.length > 0; + }); + } catch (error) { + const message = shutdown.signal.aborted ? binding.lastError : error instanceof Error && 'status' in error ? error.message : 'Skill import failed. Retry from Skills.'; + await db.update(projectSkillSources).set({ status: shutdown.signal.aborted ? 'pending' : 'error', lastError: message, leaseToken: null, leaseExpiresAt: null, + ...(error instanceof Error && 'status' in error && error.status === 403 ? { active: false } : {}), + failureCount: shutdown.signal.aborted ? binding.failureCount : binding.failureCount + 1, nextSyncAt: shutdown.signal.aborted ? now : skillSourceRetryAt(error, binding.failureCount, now) }) + .where(and(eq(projectSkillSources.id, binding.id), eq(projectSkillSources.leaseToken, token))); + return false; + } + } + + async function sweep(now: Date) { + const activeCompanies = await db.select({ id: companies.id }).from(companies).where(eq(companies.status, 'active')); + const ids = activeCompanies.map(company => company.id); + if (!ids.length) return { imported: 0, checked: 0 }; + for (const id of ids) await admitDeferredSkillImports(db, id, now); + const pending = await db.select().from(projectSkillSources).where(and(inArray(projectSkillSources.companyId, ids), + eq(projectSkillSources.active, true), isNull(projectSkillSources.sourceId), lte(projectSkillSources.nextSyncAt, now), + or(isNull(projectSkillSources.leaseToken), lte(projectSkillSources.leaseExpiresAt, now)))) + .orderBy(asc(projectSkillSources.nextSyncAt)).limit(10); + let imported = 0, checked = 0; + for (const binding of pending) { if (shutdown.signal.aborted) break; if (await importBinding(binding, now)) imported++; } + const due = await db.select().from(companySkillSources).where(and(inArray(companySkillSources.companyId, ids), + eq(companySkillSources.enabled, true), eq(companySkillSources.automaticUpdates, true), lte(companySkillSources.nextSyncAt, now), + or(isNull(companySkillSources.leaseToken), lte(companySkillSources.leaseExpiresAt, now)), + or(eq(companySkillSources.accessScope, 'company'), sql`exists (select 1 from ${projectSkillSources} b where b.source_id = ${companySkillSources.id} and b.company_id = ${companySkillSources.companyId} and b.active)`))) + .orderBy(asc(companySkillSources.nextSyncAt)).limit(10); + for (const entry of due) { + if (shutdown.signal.aborted) break; + const source = await recoverSetup(entry); + try { + const context = await contextFor(source); + if (!options.context) { + try { await skillAccessService(db, setupActor(source)).assertSource(source.companyId, source.id); } + catch (error) { + if (!(error instanceof Error && 'status' in error && error.status === 404)) throw error; + throw forbidden('The person who connected this source no longer has project access. Choose another GitHub connection.'); + } + } + await sources.refresh(source.companyId, source.id, context, undefined, undefined, { checkCommit: true, automatic: true, now, signal: shutdown.signal }); + checked++; + } catch (error) { + if (error instanceof Error && 'status' in error && error.status === 409) continue; + // Failures in context or policy authorization precede the refresh lease. + // Compare the attempt and revision so another worker's result wins. + if (!shutdown.signal.aborted) { + await db.update(companySkillSources).set({ lastError: error instanceof Error && 'status' in error ? error.message : 'Automatic update failed. The installed skills were kept.', + failureCount: source.failureCount + 1, nextSyncAt: skillSourceRetryAt(error, source.failureCount, now), lastAttemptAt: now }) + .where(and(eq(companySkillSources.id, source.id), eq(companySkillSources.revision, source.revision), + eq(companySkillSources.failureCount, source.failureCount), + source.lastAttemptAt ? eq(companySkillSources.lastAttemptAt, source.lastAttemptAt) : isNull(companySkillSources.lastAttemptAt), + or(isNull(companySkillSources.leaseToken), lte(companySkillSources.leaseExpiresAt, now)))); + } + } + } + return { imported, checked }; + } + async function refreshSource(companyId: string, sourceId: string, initiator: SkillSourceContext) { + const [source] = await db.select().from(companySkillSources).where(and(eq(companySkillSources.companyId, companyId), eq(companySkillSources.id, sourceId))); + if (!source) throw notFound('Skill source not found'); + await initiator.authorize('skills.update', { sourceType: 'git', sourceLocator: source.repositoryUrl }); + if (!source.refreshUserId && !source.refreshLocalTrusted) return sources.refresh(companyId, sourceId, initiator); + const setup = await contextFor(source); + if (!options.context) { + try { await skillAccessService(db, setupActor(source)).assertSource(companyId, sourceId); } + catch (error) { + if (!(error instanceof Error && 'status' in error && error.status === 404)) throw error; + throw forbidden('The person who connected this source no longer has project access. Choose another GitHub connection.'); + } + } + return sources.refresh(companyId, sourceId, { ...setup, actor: initiator.actor, + publishCommittedActivity: initiator.publishCommittedActivity, + authorize: async (action, resource) => { await initiator.authorize(action, resource); await setup.authorize(action, resource); }, + audit: async (tx, id, action, details) => initiator.audit(tx, id, action, { ...details, refreshUserId: source.refreshUserId }), + }); + } + function tick(now = new Date()) { + if (shutdown.signal.aborted) return Promise.resolve({ imported: 0, checked: 0 }); + if (running) return running; + running = sweep(now).finally(() => { running = null; }); + return running; + } + return { tick, refreshSource, stop: async () => { shutdown.abort(); await running; } }; +} diff --git a/server/src/services/skill-sources.ts b/server/src/services/skill-sources.ts index dae360449c2..bd8637bdbd5 100644 --- a/server/src/services/skill-sources.ts +++ b/server/src/services/skill-sources.ts @@ -1,23 +1,30 @@ +import { reserveAutomaticSkillWork, settleAutomaticSkillWork } from './skill-source-refresh-budget.js'; import { createHash, randomUUID } from 'node:crypto'; +import { skillSourceRetryAt } from './skill-source-retry.js'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import { folderService } from './folders.js'; import { and, eq, isNull, lt, or, sql } from 'drizzle-orm'; -import { companySkillSources as sources, companySkillSourceEntries as entries, companySkills, companySkillRepositorySnapshots, authUsers, type Db } from '@paperclipai/db'; -import type { CompanySkill, SkillSourceDisconnectRequest, SkillSourceDisconnectResult, SkillSource, SkillSourceCreateRequest, SkillSourceDiscoveryRequest, SkillSourceSelectionRequest, SkillSourceRefreshResult, SkillSourcePreviewRequest } from '@paperclipai/shared'; +import { companySkillSources as sources, companySkillSourceEntries as entries, companySkills, companySkillRepositorySnapshots, projectSkillSources, projects, type Db, authUsers } from '@paperclipai/db'; +import type { CompanySkill, SkillSource, SkillSourceCreateRequest, SkillSourceDiscoveryRequest, SkillSourceSelectionRequest, SkillSourceRefreshResult, SkillSourcePreviewRequest, SkillSourceSettingsRequest, SkillSourceDisconnectRequest, SkillSourceDisconnectResult } from '@paperclipai/shared'; import { normalizeAgentUrlKey } from '@paperclipai/shared'; import { conflict, notFound, unprocessable } from '../errors.js'; import { companySkillService, parseSkillImportSourceInput } from './company-skills.js'; -import { scanGitHubSkills, previewGitHubSkillFile, type GitHubRead, type ScannedSkillSource, type SkillScanOptions } from './github-skill-source.js'; +import { scanGitHubSkillsWithPackageFallback as scanGitHubSkills, rejectIncompleteSkillPackages, previewGitHubSkillFile, type GitHubRead, type ScannedSkillSource, type SkillScanOptions } from './github-skill-source.js'; +import { skillAccessService } from './skill-access.js'; import { skillSnapshotHash } from './skill-snapshot.js'; type Tx = Parameters[0]>[0]; type SourceRow = typeof sources.$inferSelect; export type SkillSourceContext = { actor: { type: 'user' | 'agent' | 'system'; userId?: string | null; agentId?: string | null }; + setup?: { userId: string | null; agentId?: string | null; localTrusted: boolean; grantId?: string | null }; + projectBindingId?: string; + projectBindingLeaseToken?: string; read: (connectionId: string | null) => GitHubRead; authorize: (action: 'skills.import' | 'skills.update' | 'skills.edit' | 'skills.remove' | 'skills.create', resource: { skillId?: string; skillKey?: string; sourceType: 'git'; sourceLocator: string }) => Promise; audit: (tx: Tx, sourceId: string, action: string, details: Record) => Promise; + publishCommittedActivity?: () => void; }; const scope = (companyId: string, id: string) => and(eq(sources.companyId, companyId), eq(sources.id, id)); @@ -25,17 +32,32 @@ export function skillSourceService(db: Db) { const skills = companySkillService(db); async function row(companyId: string, id: string, database: Db | Tx = db) { const [source] = await database.select().from(sources).where(scope(companyId, id)); - if (!source) throw notFound('Skill source not found.'); + if (!source || source.removedAt) throw notFound('Skill source not found.'); return source; } async function detail(companyId: string, id: string, database: Db | Tx = db): Promise { - const { leaseToken: _token, leaseExpiresAt: _expires, createdAt: _created, ...source } = await row(companyId, id, database); + await skillAccessService(database as Db).assertSource(companyId, id); + const { leaseToken, leaseExpiresAt, createdAt: _created, removedAt: _removed, refreshUserId: _user, refreshAgentId: _agent, refreshLocalTrusted: _local, refreshGrantId: _grant, failureCount: _failures, nextSyncAt: _next, lastScanTreeSha: _tree, automaticBudgetBytes: _bytes, automaticBudgetAt: _budgetAt, ...source } = await row(companyId, id, database); + const permitted = await skillAccessService(database as Db).projectIds(companyId, id); + const associated = await database.select({ id: projects.id, name: projects.name, visibility: projects.visibility, active: projectSkillSources.active }) + .from(projectSkillSources).innerJoin(projects, eq(projects.id, projectSkillSources.projectId)) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.sourceId, id))); + const visibleProjects = new Map(); + for (const project of associated) { + if (!permitted.some(binding => binding.projectId === project.id)) continue; + const previous = visibleProjects.get(project.id); + visibleProjects.set(project.id, { ...project, active: project.active || Boolean(previous?.active) }); + } + const refreshing = leaseToken && leaseExpiresAt && leaseExpiresAt > new Date(); + const trackingActive = source.accessScope === 'company' || associated.some(project => project.active); const selected = await database.select().from(entries).where(and(eq(entries.companyId, companyId), eq(entries.sourceId, id))).orderBy(entries.path); - return { ...source, entries: selected }; + return { ...source, trackingActive, entries: selected, projects: [...visibleProjects.values()].map(project => ({ ...project, visibility: project.visibility as 'open' | 'private' })), + syncState: !source.enabled || !source.automaticUpdates || !trackingActive ? 'paused' : refreshing ? 'refreshing' : source.lastError ? 'error' : source.lastSuccessAt ? 'ready' : 'pending' }; } async function list(companyId: string) { - const rows = await db.select({ id: sources.id }).from(sources).where(eq(sources.companyId, companyId)).orderBy(sources.fullName); - return Promise.all(rows.map(source => detail(companyId, source.id))); + const rows = await db.select({ id: sources.id }).from(sources).where(and(eq(sources.companyId, companyId), isNull(sources.removedAt))).orderBy(sources.fullName); + const visible = await skillAccessService(db).visibleSourceIds(companyId); + return Promise.all(rows.filter(source => visible === null || visible.includes(source.id)).map(source => detail(companyId, source.id))); } async function sourceForSkill(companyId: string, skillId: string) { const [entry] = await db.select().from(entries).where(and(eq(entries.companyId, companyId), eq(entries.skillId, skillId))); @@ -43,7 +65,7 @@ export function skillSourceService(db: Db) { } async function discover(input: SkillSourceDiscoveryRequest, context: SkillSourceContext, options?: SkillScanOptions) { await context.authorize('skills.import', { sourceType: 'git', sourceLocator: input.repositoryUrl }); - const { skills: _files, repositoryFiles: _repositoryFiles, defaultBranch: _defaultBranch, ...result } = await scanGitHubSkills(input, context.read(input.connectionId ?? null), { ...options, retainFiles: false }); + const { skills: _files, repositoryFiles: _repositoryFiles, defaultBranch: _defaultBranch, treeSha: _tree, downloadBytes: _download, scanBytes: _scan, ...result } = await scanGitHubSkills(input, context.read(input.connectionId ?? null), { ...options, retainFiles: false }); return result; } async function preview(input: SkillSourcePreviewRequest, context: SkillSourceContext) { @@ -56,7 +78,7 @@ export function skillSourceService(db: Db) { const old = previous.find(entry => entry.path === candidate.path); const skill = old?.skillId ? await skills.getById(source.companyId, old.skillId) : null; await context.authorize(skill ? 'skills.update' : 'skills.import', { - sourceType: 'git', sourceLocator: source.repositoryUrl, ...(skill ? { skillId: skill.id, skillKey: skill.key } : { skillKey: newSkillKey(scan, candidate.path, candidate.name, source.id) }), + sourceType: 'git', sourceLocator: source.repositoryUrl, ...(skill ? { skillId: skill.id, skillKey: skill.key } : { skillKey: newSkillKey(scan, candidate.path, candidate.name, source.id, source.revision) }), }); } } @@ -67,17 +89,19 @@ export function skillSourceService(db: Db) { if (skill) await context.authorize('skills.edit', { sourceType: 'git', sourceLocator: source.repositoryUrl, skillId: skill.id, skillKey: skill.key }); } } - function newSkillKey(scan: ScannedSkillSource, skillPath: string, name: string, sourceId: string) { - const identity = createHash('sha256').update(`${scan.repositoryId}:${sourceId}:${skillPath}`).digest('hex').slice(0, 16); + function newSkillKey(scan: ScannedSkillSource, skillPath: string, name: string, sourceId: string, revision = 0) { + // New imports after removal must not overwrite retained editable copies. + // Existing entries keep their installed identity across later revisions. + const identity = createHash('sha256').update(`${scan.repositoryId}:${sourceId}${revision ? `:${revision}` : ''}:${skillPath}`).digest('hex').slice(0, 16); return `github/${scan.repositoryId}/${identity}/${normalizeAgentUrlKey(name) || 'skill'}`; } async function publish(tx: Tx, source: SourceRow, scan: ScannedSkillSource, selectedPaths: string[], excludedFolders: string[], context: SkillSourceContext, selectionReviewed: boolean): Promise> { const mode = scan.packageMode ?? 'skills'; - if (mode === 'repository' && (scan.repositoryPackage?.error || !scan.repositoryFiles?.length)) { + if (mode === 'repository' && scan.skills.length > 0 && (scan.repositoryPackage?.error || !scan.repositoryFiles?.length)) { throw unprocessable(scan.repositoryPackage?.error ?? 'Repository package has no files. Scan it again before importing.'); } let repositorySnapshotId: string | undefined; - const repositoryHash = mode === 'repository' ? skillSnapshotHash(scan.repositoryFiles!) : null; + const repositoryHash = mode === 'repository' && scan.repositoryFiles?.length && !scan.repositoryPackage?.error ? skillSnapshotHash(scan.repositoryFiles) : null; if (repositoryHash) { await tx.insert(companySkillRepositorySnapshots).values({ companyId: source.companyId, contentHash: repositoryHash, fileInventory: scan.repositoryFiles! }) .onConflictDoNothing(); @@ -103,7 +127,7 @@ export function skillSourceService(db: Db) { if (selection === 'selected' && !candidate.error) { const hash = repositoryHash ? createHash('sha256').update(`${repositoryHash}:${candidate.path}`).digest('hex') : skillSnapshotHash(candidate.files); const slug = normalizeAgentUrlKey(candidate.name) || 'skill'; - const key = current?.key ?? newSkillKey(scan, candidate.path, candidate.name, source.id); + const key = current?.key ?? newSkillKey(scan, candidate.path, candidate.name, source.id, source.revision); const ownerRepo = scan.fullName.split('/'); const metadata = { ...(current?.metadata ?? {}), sourceKind: 'github', hostname: 'github.com', owner: ownerRepo[0], repo: ownerRepo[1], ref: current?.metadata?.snapshotHash === hash && current.currentVersionId ? current.sourceRef : scan.commitSha, trackingRef: scan.trackingRef, @@ -111,6 +135,7 @@ export function skillSourceService(db: Db) { skillSourceId: source.id, skillSourcePath: candidate.path, skillSourceState: 'synced', snapshotHash: hash, packageMode: mode }; const trustFiles = mode === 'repository' ? scan.repositoryFiles! : candidate.files; const values = { name: candidate.name, description: candidate.description, markdown: candidate.files.find(file => file.path === 'SKILL.md')!.content, + accessSourceId: source.accessScope === 'projects' ? source.id : current?.accessSourceId ?? null, sourceType: 'github', sourceLocator: scan.repositoryUrl, sourceRef: current?.metadata?.snapshotHash === hash && current.currentVersionId ? current.sourceRef : scan.commitSha, fileInventory: candidate.files.map(({ path, kind }) => ({ path, kind })), trustLevel: trustFiles.some(file => file.kind === 'script' || file.executable) ? 'scripts_executables' : trustFiles.some(file => file.kind === 'asset' || file.kind === 'other') ? 'assets' : 'markdown_only', @@ -160,17 +185,20 @@ export function skillSourceService(db: Db) { eq(sources.companyId, source.companyId), eq(sources.trackingRef, scan.trackingRef), or(eq(sources.repositoryId, scan.repositoryId), eq(sources.repositoryUrl, scan.repositoryUrl)), )) : []; + const [defaultBinding] = source.trackingRef === 'HEAD' ? await tx.select({ id: projectSkillSources.id }).from(projectSkillSources) + .where(and(eq(projectSkillSources.companyId, source.companyId), eq(projectSkillSources.sourceId, source.id), eq(projectSkillSources.trackingRef, 'HEAD'))).limit(1) : []; await tx.update(sources).set({ repositoryId: scan.repositoryId, repositoryUrl: scan.repositoryUrl, fullName: scan.fullName, packageMode: mode, repositoryPackage: scan.repositoryPackage ?? null, - trackingRef: existingBranch ? 'HEAD' : scan.trackingRef, excludedFolders, lastSuccessAt: new Date(), lastScanCommit: scan.commitSha, lastError: null, - revision: source.revision + 1, leaseToken: null, leaseExpiresAt: null }).where(scope(source.companyId, source.id)); + trackingRef: existingBranch || defaultBinding || (source.accessScope === 'projects' && source.trackingRef === 'HEAD') ? 'HEAD' : scan.trackingRef, excludedFolders, lastSuccessAt: new Date(), lastScanCommit: scan.commitSha, lastScanTreeSha: scan.treeSha ?? null, lastError: null, + revision: source.revision + 1, leaseToken: null, leaseExpiresAt: null, failureCount: 0, nextSyncAt: new Date(Date.now() + 5 * 60_000) }).where(scope(source.companyId, source.id)); await context.audit(tx, source.id, 'company.skill_source_refreshed', { commit: scan.commitSha, importedCount: imported.length, updatedCount: updated.length, unchanged, warningCount: warnings.length }); return { imported, updated, unchanged, warnings: [...new Set(warnings)] }; } async function create(companyId: string, input: SkillSourceCreateRequest, context: SkillSourceContext, staged?: ScannedSkillSource) { await context.authorize('skills.import', { sourceType: 'git', sourceLocator: input.repositoryUrl }); - const scan = staged ?? await scanGitHubSkills(input, context.read(input.connectionId ?? null)); - if (scan.trackingRef === scan.defaultBranch && (await list(companyId)).some(source => source.repositoryUrl.toLowerCase() === scan.repositoryUrl.toLowerCase() && source.trackingRef === 'HEAD')) { + const reader = context.read(input.connectionId ?? null); + const scan = staged ?? await scanGitHubSkills(input, reader); + if (!context.projectBindingId && scan.trackingRef === scan.defaultBranch && (await list(companyId)).some(source => source.repositoryUrl.toLowerCase() === scan.repositoryUrl.toLowerCase() && source.trackingRef === 'HEAD')) { throw conflict('This repository is already in Sources. Manage its skills there.'); } const id = randomUUID(); @@ -179,15 +207,24 @@ export function skillSourceService(db: Db) { await context.authorize('skills.import', { sourceType: 'git', sourceLocator: scan.repositoryUrl, skillKey: newSkillKey(scan, candidate.path, candidate.name, id) }); } - return db.transaction(async tx => { + const published = await db.transaction(async tx => { const [source] = await tx.insert(sources).values({ id, companyId, repositoryId: scan.repositoryId, repositoryUrl: scan.repositoryUrl, fullName: scan.fullName, - packageMode: scan.packageMode ?? 'skills', trackingRef: scan.trackingRef, connectionId: scan.connectionId ?? input.connectionId ?? null, lastAttemptAt: new Date() }).onConflictDoNothing().returning(); + packageMode: scan.packageMode ?? 'skills', trackingRef: context.projectBindingId && input.trackingRef === 'HEAD' ? 'HEAD' : scan.trackingRef, connectionId: scan.connectionId ?? input.connectionId ?? null, lastAttemptAt: new Date(), + accessScope: context.projectBindingId ? 'projects' : 'company', importNewSkills: Boolean(context.projectBindingId), + refreshUserId: context.setup?.userId, refreshAgentId: context.setup?.agentId, refreshLocalTrusted: context.setup?.localTrusted ?? false, refreshGrantId: reader.grantId ?? context.setup?.grantId }).onConflictDoNothing().returning(); if (!source) throw conflict('This repository and branch are already in Sources. Manage its skills there.'); + if (context.projectBindingId) { + const [binding] = await tx.select().from(projectSkillSources).where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.id, context.projectBindingId))).for('update'); + if (!binding?.active || (context.projectBindingLeaseToken && binding.leaseToken !== context.projectBindingLeaseToken)) throw conflict('Project repository changed during import.'); + await tx.update(projectSkillSources).set({ sourceId: source.id, status: 'ready', lastError: null }).where(eq(projectSkillSources.id, binding.id)); + } const result = await publish(tx, source, scan, input.selectedPaths, input.excludedFolders ?? [], context, true); return { ...result, source: await detail(companyId, id, tx) }; }); + context.publishCommittedActivity?.(); + return published; } - async function refresh(companyId: string, id: string, context: SkillSourceContext, selection?: SkillSourceSelectionRequest, staged?: ScannedSkillSource) { + async function refresh(companyId: string, id: string, context: SkillSourceContext, selection?: SkillSourceSelectionRequest, staged?: ScannedSkillSource, options: { checkCommit?: boolean; automatic?: boolean; now?: Date; signal?: AbortSignal } = {}) { let source = await row(companyId, id); if (!source.enabled && !selection) throw conflict('This source is disconnected. Save its selection to reconnect.'); await context.authorize(selection ? 'skills.edit' : 'skills.update', { sourceType: 'git', sourceLocator: source.repositoryUrl }); @@ -200,25 +237,94 @@ export function skillSourceService(db: Db) { source = leased; try { const connectionId = selection?.connectionId !== undefined ? selection.connectionId : source.connectionId; - const scan = staged ?? await scanGitHubSkills({ repositoryUrl: source.repositoryUrl, trackingRef: source.trackingRef, packageMode: selection?.packageMode ?? source.packageMode }, context.read(connectionId)); + const reader = context.read(connectionId); + if (options.checkCommit && source.repositoryId && source.lastScanCommit) { + const repo = await reader(`/repos/${source.fullName}`, options.signal) as { id: number; default_branch: string }; + if (String(repo.id) !== source.repositoryId) throw conflict('The repository at this URL has changed identity. Add it as a new source.'); + const ref = source.trackingRef === 'HEAD' ? repo.default_branch : source.trackingRef; + const head = await reader(`/repos/${source.fullName}/commits/${encodeURIComponent(ref)}`, options.signal) as { sha: string; commit?: { tree?: { sha?: string } } }; + const rawTreeSha = head.commit?.tree?.sha; + const treeSha = rawTreeSha && /^[a-f0-9]{40}$/i.test(rawTreeSha) ? rawTreeSha.toLowerCase() : null; + if (head.sha === source.lastScanCommit || (treeSha && treeSha === source.lastScanTreeSha)) { + await db.transaction(async tx => { + const [locked] = await tx.select().from(sources).where(scope(companyId, id)).for('update'); + if (!locked || locked.leaseToken !== token || locked.revision !== source.revision) throw conflict('Source refresh changed. Try again.'); + await context.audit(tx, id, 'company.skill_source_checked', { commit: head.sha, unchangedTree: head.sha !== source.lastScanCommit }); + await tx.update(sources).set({ leaseToken: null, leaseExpiresAt: null, lastError: null, lastSuccessAt: new Date(), failureCount: 0, lastScanCommit: head.sha, lastScanTreeSha: treeSha ?? source.lastScanTreeSha, + connectionId: reader.connectionId ?? source.connectionId, refreshGrantId: reader.grantId ?? source.refreshGrantId, + nextSyncAt: new Date(Date.now() + 5 * 60_000) }).where(scope(companyId, id)); + }); + context.publishCommittedActivity?.(); + return { source: await detail(companyId, id), imported: [], updated: [], unchanged: (await detail(companyId, id)).entries.filter(entry => entry.selection === 'selected').length, warnings: [] }; + } + } + const packageMode = selection?.packageMode ?? (source.accessScope === 'projects' ? 'repository' : source.packageMode); + if (options.automatic) await reserveAutomaticSkillWork(db, source, token, options.now ?? new Date()); + const scan = staged ?? await scanGitHubSkills({ repositoryUrl: source.repositoryUrl, trackingRef: source.trackingRef, packageMode }, reader, { signal: options.signal }); + if (options.automatic) await settleAutomaticSkillWork(db, companyId, id, token, + packageMode === 'repository' && scan.packageMode === 'skills' ? {} : scan); + // A project source may already be in package-only mode after initial fallback. + if (source.accessScope === 'projects' && scan.packageMode === 'skills') rejectIncompleteSkillPackages(scan); if (source.repositoryId && source.repositoryId !== scan.repositoryId) throw conflict('The repository at this URL has changed identity. Add it as a new source.'); const current = await detail(companyId, id); + const omittedInstalledFiles = current.entries.some(entry => entry.skillId && current.repositoryPackage?.files.some(file => { + const root = path.posix.dirname(entry.path); + return root !== '.' && !file.path.startsWith(`${root}/`); + })); + if (source.packageMode === 'repository' && scan.packageMode === 'skills' + && selection?.packageMode !== 'skills' && current.entries.some(entry => entry.skillId) + && (omittedInstalledFiles || scan.skills.some(skill => skill.inspection?.references.length))) { + throw unprocessable('The repository exceeds the supported package size. Installed repository skills and their dependencies were kept. Reduce the repository package size before refreshing.'); + } const selectedPaths = selection?.selectedPaths ?? current.entries.filter(entry => entry.selection === 'selected').map(entry => entry.path); + if (!selection && source.importNewSkills) { + for (const candidate of scan.skills) { + if (current.entries.some(entry => entry.path === candidate.path) || source.excludedFolders.some(folder => !folder || candidate.path.startsWith(`${folder}/`))) continue; + selectedPaths.push(candidate.path); + } + } await authorizeScan(source, scan, selectedPaths, context); - return await db.transaction(async tx => { + const published = await db.transaction(async tx => { const [locked] = await tx.select().from(sources).where(scope(companyId, id)).for('update'); if (!locked || locked.leaseToken !== token || locked.revision !== source.revision || locked.leaseExpiresAt! < new Date()) throw conflict('Source refresh expired or changed. Try again.'); const result = await publish(tx, locked, scan, selectedPaths, selection?.excludedFolders ?? source.excludedFolders, context, Boolean(selection)); - if (selection || scan.connectionId !== undefined) await tx.update(sources).set({ ...(selection ? { enabled: true } : {}), connectionId: scan.connectionId ?? connectionId }).where(scope(companyId, id)); + if (selection || scan.connectionId !== undefined) await tx.update(sources).set({ ...(selection ? { enabled: true } : {}), connectionId: scan.connectionId ?? connectionId, + ...(source.refreshGrantId === null && reader.grantId ? { refreshGrantId: reader.grantId } : {}), + ...(selection?.connectionId !== undefined || (!source.refreshUserId && !source.refreshLocalTrusted && context.setup) ? { refreshUserId: context.setup?.userId, refreshAgentId: context.setup?.agentId ?? null, refreshLocalTrusted: context.setup?.localTrusted ?? false, refreshGrantId: reader.grantId ?? null } : {}) }).where(scope(companyId, id)); return { ...result, source: await detail(companyId, id, tx) }; }); + context.publishCommittedActivity?.(); + return published; } catch (error) { // Provider errors have already been sanitized. Never persist arbitrary exception text. const message = error instanceof Error && 'status' in error ? error.message : 'Refresh failed. The installed skills were kept. Try again.'; - await db.update(sources).set({ leaseToken: null, leaseExpiresAt: null, lastError: message }).where(and(scope(companyId, id), eq(sources.leaseToken, token))); + await db.update(sources).set({ leaseToken: null, leaseExpiresAt: null, lastError: options.signal?.aborted ? source.lastError : message, failureCount: options.signal?.aborted ? source.failureCount : source.failureCount + 1, nextSyncAt: options.signal?.aborted ? new Date() : skillSourceRetryAt(error, source.failureCount) }).where(and(scope(companyId, id), eq(sources.leaseToken, token))); throw error; } } + async function settings(companyId: string, id: string, input: SkillSourceSettingsRequest, context: SkillSourceContext) { + const source = await row(companyId, id); + await context.authorize('skills.edit', { sourceType: 'git', sourceLocator: source.repositoryUrl }); + // Validate a replacement connection before binding unattended work to it. + let reader: GitHubRead | undefined; + if (input.connectionId !== undefined) { + reader = context.read(input.connectionId); + await reader(`/repos/${source.fullName}`); + } + return db.transaction(async tx => { + const [updated] = await tx.update(sources).set({ + ...(input.automaticUpdates !== undefined ? { automaticUpdates: input.automaticUpdates } : {}), + ...(input.importNewSkills !== undefined ? { importNewSkills: input.importNewSkills } : {}), + ...(input.connectionId !== undefined ? { connectionId: reader?.connectionId ?? input.connectionId, + refreshUserId: context.setup?.userId, refreshAgentId: context.setup?.agentId ?? null, + refreshLocalTrusted: context.setup?.localTrusted ?? false, refreshGrantId: reader?.grantId ?? null, lastError: null } : {}), + revision: sql`${sources.revision} + 1`, nextSyncAt: new Date(), + }).where(and(scope(companyId, id), eq(sources.revision, input.revision), or(isNull(sources.leaseToken), lt(sources.leaseExpiresAt, new Date())))).returning(); + if (!updated) throw conflict('This source is refreshing or changed. Reload before saving.'); + await context.audit(tx, id, 'company.skill_source_settings_updated', { automaticUpdates: updated.automaticUpdates, importNewSkills: updated.importNewSkills, connectionChanged: input.connectionId !== undefined }); + return detail(companyId, id, tx); + }); + } async function disconnect(companyId: string, id: string, context: SkillSourceContext, input: SkillSourceDisconnectRequest): Promise { const source = await detail(companyId, id); if (source.revision !== input.revision) throw conflict('This source changed. Reload before disconnecting.'); @@ -236,7 +342,18 @@ export function skillSourceService(db: Db) { if (!locked || locked.revision !== input.revision) throw conflict('This source changed. Reload before disconnecting.'); } async function remove(tx: Tx, copiedCount: number, deletedCount: number) { - await tx.delete(sources).where(scope(companyId, id)); + // Stop both pending attachments and an in-flight refresh before removing tracking. + await tx.update(projectSkillSources).set({ active: false, leaseToken: null, leaseExpiresAt: null }) + .where(and(eq(projectSkillSources.companyId, companyId), eq(projectSkillSources.sourceId, id))); + if (source.accessScope === 'projects') { + // Copies and forks keep this source identity as their dynamic access boundary. + await tx.delete(entries).where(and(eq(entries.companyId, companyId), eq(entries.sourceId, id))); + await tx.update(sources).set({ removedAt: new Date(), enabled: false, automaticUpdates: false, + revision: sql`${sources.revision} + 1`, leaseToken: null, leaseExpiresAt: null }) + .where(scope(companyId, id)); + } else { + await tx.delete(sources).where(scope(companyId, id)); + } await context.audit(tx, id, 'company.skill_source_disconnected', { skills: input.skills, skillIds, copiedCount, deletedCount }); } if (input.skills === 'delete') { @@ -305,5 +422,5 @@ export function skillSourceService(db: Db) { const imported = await Promise.all(result.source.entries.filter(entry => selectedPaths.includes(entry.path) && entry.skillId && !entry.error).map(entry => skills.getById(companyId, entry.skillId!))); return { imported: imported.filter((skill): skill is CompanySkill => Boolean(skill)), warnings: result.warnings }; } - return { list, detail, sourceForSkill, discover, preview, create, refresh, disconnect, importFromUrl }; + return { list, detail, sourceForSkill, discover, preview, create, refresh, settings, disconnect, importFromUrl }; } diff --git a/tests/e2e/signoff-policy.spec.ts b/tests/e2e/signoff-policy.spec.ts index da005094f2d..9bdb0f4ad10 100644 --- a/tests/e2e/signoff-policy.spec.ts +++ b/tests/e2e/signoff-policy.spec.ts @@ -275,6 +275,7 @@ async function waitForFixtureRun( agentId: string, issueId: string, runId: string, + allowCancelledLock = false, ) { // Negative authorization cases deliberately invoke a non-participant. Leave // their API rejection intact instead of waiting for a run they cannot own. @@ -286,7 +287,8 @@ async function waitForFixtureRun( const run = await fetchVerifiedRun(board, runId, agentId, issueId); expect(run, "Fixture run must belong to the acting agent and issue").not.toBeNull(); status = run!.status; - if (status === "succeeded") return; + if (status === "succeeded") return true; + if (status === "cancelled" && allowCancelledLock) return false; expect(["queued", "running"], `Fixture run ${runId} ended as ${status}`).toContain(status); await new Promise((resolve) => setTimeout(resolve, WAKEUP_POLL_INTERVAL_MS)); } while (Date.now() < deadline); @@ -311,7 +313,9 @@ async function retryAgentPatchWithCurrentLockOnConflict( const lockedRunId = issueRunLock.checkoutRunId ?? issueRunLock.executionRunId ?? fallbackRunId; // A new lock can belong to a newly promoted fixture run, not the completed // run we first invoked. Let that exact verified run settle before PATCH. - await waitForFixtureRun(board, agent.agentId, issueId, lockedRunId); + // Recovery can cancel the selected lock while this wait is in flight. + // Read the current lock again; never PATCH under that cancelled run. + if (!(await waitForFixtureRun(board, agent.agentId, issueId, lockedRunId, true))) continue; res = await agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, { headers: { "X-Paperclip-Run-Id": lockedRunId }, data: patchData, @@ -368,7 +372,7 @@ async function agentPatch( // run-lock race — leave it for the caller to assert on. if (issueRunLock.assigneeAgentId !== agent.agentId) break; const retryRunId = issueRunLock.checkoutRunId ?? issueRunLock.executionRunId ?? runId; - await waitForFixtureRun(board, agent.agentId, issueId, retryRunId); + if (!(await waitForFixtureRun(board, agent.agentId, issueId, retryRunId, true))) continue; res = await patchWith(retryRunId); } return res; diff --git a/tests/storybook-visual/playwright.config.ts b/tests/storybook-visual/playwright.config.ts index 679f048f4f6..e778bc7f16d 100644 --- a/tests/storybook-visual/playwright.config.ts +++ b/tests/storybook-visual/playwright.config.ts @@ -47,9 +47,9 @@ export default defineConfig({ // JS-driven tickers/timers key off prefers-reduced-motion for // deterministic captures (CSS animations are already disabled). reducedMotion: "reduce", - baseURL: "http://localhost:6106", + baseURL: process.env.STORYBOOK_TEST_BASE_URL ?? "http://localhost:6106", }, - webServer: { + webServer: process.env.STORYBOOK_TEST_BASE_URL ? undefined : { command: "node ../../scripts/serve-storybook-static.mjs --port 6106", url: "http://localhost:6106/index.json", reuseExistingServer: true, diff --git a/tests/storybook-visual/project-repository-skills.spec.ts b/tests/storybook-visual/project-repository-skills.spec.ts new file mode 100644 index 00000000000..0d02800e406 --- /dev/null +++ b/tests/storybook-visual/project-repository-skills.spec.ts @@ -0,0 +1,202 @@ +import { expect, test } from '@playwright/test'; + +const story = (name: string) => `/iframe.html?id=skills-project-repository-skills-02-journeys--${name}&viewMode=story&globals=theme:dark`; + +test('creating a project imports repository skills without another setup flow', async ({ page }) => { + await page.goto(story('create-project')); + await page.getByRole('button', { name: 'Add Project', exact: true }).first().click(); + const dialog = page.getByRole('dialog', { name: 'Create project', exact: true }); + await dialog.getByRole('textbox', { name: 'Project name', exact: true }).fill('Repository project'); + await dialog.getByRole('button', { name: 'Add GitHub repo', exact: true }).click(); + await page.getByRole('option', { name: 'acme/team-skills My GitHub account · Acme engineering', exact: true }).click(); + await dialog.getByRole('button', { name: 'Create project', exact: true }).click(); + await expect(dialog).toHaveCount(0); + await page.getByRole('link', { name: 'Skills', exact: true }).click(); + await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Sources', exact: true }).click(); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await expect(source.getByText('Code review', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Import skills', exact: true })).toHaveCount(0); +}); + +test('attaching another repository imports skills and keeps the original source', async ({ page }) => { + await page.goto(story('attach-repository')); + await page.getByRole('button', { name: 'Add another repo', exact: true }).click(); + await page.getByRole('option', { name: 'maya/research-skills My GitHub account', exact: true }).click(); + await page.getByRole('button', { name: 'Save changes', exact: true }).click(); + await expect(page.getByText('Changes saved', { exact: true })).toBeVisible(); + await page.getByRole('link', { name: 'Skills', exact: true }).click(); + await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Sources', exact: true }).click(); + const original = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + const added = page.getByRole('region', { name: 'maya/research-skills', exact: true }); + await expect(original.getByText('Code review', { exact: true })).toBeVisible(); + await expect(added.getByText('Code review', { exact: true })).toBeVisible(); + expect(await original.getByRole('link', { name: /^Code review / }).getAttribute('href')) + .not.toBe(await added.getByRole('link', { name: /^Code review / }).getAttribute('href')); +}); + +test('empty repositories explain automatic discovery of future skills', async ({ page }) => { + await page.goto(story('empty-repository')); + await expect(page.getByText('No skills found yet. New skills will be imported automatically.', { exact: true })).toBeVisible(); +}); + +test('routine instructions stay stable while new executions follow updated skill content', async ({ page }) => { + await page.goto(story('routine-reference')); + const description = page.getByRole('region', { name: 'Description', exact: true }); + await expect(description.getByRole('link', { name: '/code-review', exact: true })).toBeVisible(); + const instructions = await description.innerText(); + await page.getByRole('button', { name: 'Run now', exact: true }).click(); + await page.getByRole('button', { name: 'Run routine', exact: true }).click(); + await page.getByRole('link', { name: 'Open PAP-124: Review project changes', exact: true }).click(); + const outcome = page.getByText(/^Completed using code review version /); + await expect(outcome).toContainText('version-entry-1 at commit a17d36'); + await expect(outcome).toBeVisible(); + const first = await outcome.textContent() ?? ''; + expect(first).toContain('version-entry-1 at commit a17d36'); + await expect(page.getByRole('alert')).toHaveCount(0); + await page.getByRole('link', { name: 'Skills', exact: true }).click(); + await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Sources', exact: true }).click(); + await page.getByRole('region', { name: 'acme/team-skills', exact: true }).getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(page.getByText('Security review', { exact: true })).toBeVisible(); + await page.getByRole('link', { name: 'Routines', exact: true }).click(); + await page.getByRole('main').getByText('Review project changes', { exact: true }).click(); + await page.getByLabel('Routine navigation', { exact: true }).getByRole('link', { name: 'Overview', exact: true }).click(); + await expect(description).toHaveText(instructions, { useInnerText: true }); + await page.getByRole('button', { name: 'Run now', exact: true }).click(); + await page.getByRole('button', { name: 'Run routine', exact: true }).click(); + await page.getByRole('link', { name: 'Open PAP-125: Review project changes', exact: true }).click(); + await expect(outcome).toContainText('version-entry-1-updated at commit bbbbbbbb'); + await page.getByRole('link', { name: 'Routine', exact: true }).click(); + await page.getByLabel('Routine navigation', { exact: true }).getByRole('link', { name: 'Runs', exact: true }).click(); + await page.getByRole('link', { name: 'Open PAP-124: Review project changes', exact: true }).click(); + await expect(outcome).toHaveText(first); +}); + +test('legacy adoption imports directly and survives navigation within the journey', async ({ page }, info) => { + await page.goto(story('legacy-adoption')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + const action = page.getByRole('button', { name: 'Import skills', exact: true }); + await action.click(); + await expect(source).toBeVisible(); + await expect(source.getByText('Code review', { exact: true })).toBeVisible(); + await expect(action).toHaveCount(0); + await page.getByRole('link', { name: 'Installed skills', exact: true }).click(); + await expect(source).toHaveCount(0); + await expect(page.getByText('Code review', { exact: true }).first()).toBeVisible(); + await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Sources', exact: true }).click(); + await expect(source).toBeVisible(); + await page.screenshot({ path: info.outputPath('legacy-adopted.png') }); +}); + +test('background refresh imports an addition without clicking refresh', async ({ page }, info) => { + await page.goto(story('automatic-update')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await expect(source.getByText('Security review', { exact: true })).toBeVisible({ timeout: 15000 }); + await expect(source.getByRole('button', { name: 'Refresh', exact: true })).toBeEnabled(); + await page.screenshot({ path: info.outputPath('automatic-update.png') }); +}); + +test('refresh failure retains content and retry recovers', async ({ page }, info) => { + await page.goto(story('failed-refresh')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await expect(source.getByText('Code review', { exact: true })).toBeVisible(); + await expect(source.getByRole('alert')).toContainText('installed skills were kept'); + await source.getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(source.getByRole('button', { name: 'Refresh', exact: true })).toBeEnabled(); + await source.getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(source.getByText('Security review', { exact: true })).toBeVisible(); + await expect(source.getByRole('alert')).toHaveCount(0); + await page.screenshot({ path: info.outputPath('refresh-recovered.png') }); +}); + +test('automatic update preference is accessible and changes in place', async ({ page }) => { + await page.goto(story('synced')); + await page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + const toggle = page.getByRole('menuitemcheckbox', { name: 'Automatic updates', exact: true }); + await expect(toggle).toBeChecked(); + await toggle.click(); + await expect(page.getByText('Automatic updates paused', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + await expect(toggle).not.toBeChecked(); + await page.keyboard.press('Escape'); + await expect(page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true })).toBeFocused(); +}); + +test('mobile touch actions stay usable without horizontal overflow', async ({ browser }, info) => { + const context = await browser.newContext({ baseURL: info.project.use.baseURL, viewport: { width: 390, height: 844 }, hasTouch: true, reducedMotion: 'reduce' }); + const page = await context.newPage(); + try { + await page.goto(story('mobile')); + await expect(page.getByRole('heading', { name: 'Skill sources', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh', exact: true })).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); + await page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).tap(); + await page.getByRole('menuitemcheckbox', { name: 'Automatic updates', exact: true }).tap(); + await expect(page.getByText('Automatic updates paused', { exact: true })).toBeVisible(); + await page.screenshot({ path: info.outputPath('sources-mobile.png') }); + } finally { await context.close(); } +}); + +test('connection repair keeps the installed skills and selections', async ({ page }) => { + await page.goto(story('connection-repair')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await source.getByRole('button', { name: 'Choose connection', exact: true }).click(); + const dialog = page.getByRole('dialog'); + await expect(dialog.getByRole('button', { name: 'Save connection', exact: true })).toBeDisabled(); + await dialog.getByRole('radio', { name: 'My GitHub account', exact: true }).click(); + await dialog.getByRole('button', { name: 'Save connection', exact: true }).click(); + await expect(dialog).toHaveCount(0); + await expect(source.getByRole('button', { name: 'More actions for acme/team-skills', exact: true })).toBeFocused(); + await expect(source.getByRole('alert')).toHaveCount(0); + await expect(source.getByText('Code review', { exact: true })).toBeVisible(); +}); + +test('connection repair recovers from a concurrent update without losing the selected account', async ({ page }) => { + await page.goto(story('repair-conflict')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await source.getByRole('button', { name: 'Choose connection', exact: true }).click(); + const dialog = page.getByRole('dialog'); + const account = dialog.getByRole('radio', { name: 'My GitHub account', exact: true }); + await account.click(); + await dialog.getByRole('button', { name: 'Save connection', exact: true }).click(); + await expect(dialog.getByRole('alert')).toContainText('This source was updated'); + await expect(account).toBeChecked(); + await dialog.getByRole('button', { name: 'Save connection', exact: true }).click(); + await expect(dialog).toHaveCount(0); + await expect(source.getByRole('alert')).toHaveCount(0); + await expect(source.getByText('Code review', { exact: true })).toBeVisible(); +}); + +test('keeping project skill copies explains retained access and removes tracking', async ({ page }) => { + await page.goto(story('private-project')); + await page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Disconnect source', exact: true }).click(); + const dialog = page.getByRole('dialog', { name: 'Disconnect acme/team-skills?', exact: true }); + await expect(dialog.getByRole('button', { name: 'Disconnect source', exact: true })).toBeDisabled(); + await expect(dialog.getByText('Move them to My Skills, keeping project access restrictions. Assigned agents keep using them.', { exact: true })).toBeVisible(); + await dialog.getByRole('radio', { name: /Keep editable copies/ }).click(); + await dialog.getByRole('button', { name: 'Disconnect source', exact: true }).click(); + await expect(dialog).toHaveCount(0); + await expect(page.getByRole('region', { name: 'acme/team-skills', exact: true })).toHaveCount(0); +}); + +test('cancelling source removal keeps tracking and allows refresh', async ({ page }) => { + await page.goto(story('synced')); + await page.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Disconnect source', exact: true }).click(); + const dialog = page.getByRole('dialog', { name: 'Disconnect acme/team-skills?', exact: true }); + await dialog.getByRole('radio', { name: /Delete skills/ }).click(); + await dialog.getByRole('button', { name: 'Cancel', exact: true }).click(); + await expect(dialog).toHaveCount(0); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await source.getByRole('button', { name: 'Refresh', exact: true }).click(); + await expect(source.getByText('Security review', { exact: true })).toBeVisible(); +}); + + +test('withdrawn setup requests remain terminal on a narrow light layout', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/iframe.html?id=connections-in-task-connections--ai-withdrawn&viewMode=story&globals=theme:light'); + await expect(page.getByTestId('connection-intent-terminal')).toBeVisible(); + await expect(page.getByText('Connection request withdrawn', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Fix connection', exact: true })).toHaveCount(0); +}); diff --git a/ui/src/api/skillSources.ts b/ui/src/api/skillSources.ts index 2cf2f8ca284..1b04a2563fc 100644 --- a/ui/src/api/skillSources.ts +++ b/ui/src/api/skillSources.ts @@ -1,8 +1,11 @@ -import type { ProjectRepositoryOptions, SkillSourceDisconnectRequest, SkillSourceDisconnectResult, SkillSourceFilePreview, SkillSourcePreviewRequest, SkillSource, SkillSourceCreateRequest, SkillSourceDiscovery, SkillSourceDiscoveryRequest, SkillSourceRefreshResult, SkillSourceSelectionRequest, SkillSourceScanUpdate, SkillSourceDiscoveryEvent } from '@paperclipai/shared'; +import type { ProjectRepositoryOptions, ProjectSkillRepository, SkillSourceSettingsRequest, SkillSourceFilePreview, SkillSourcePreviewRequest, SkillSource, SkillSourceCreateRequest, SkillSourceDiscovery, SkillSourceDiscoveryRequest, SkillSourceRefreshResult, SkillSourceSelectionRequest, SkillSourceScanUpdate, SkillSourceDiscoveryEvent, SkillSourceDisconnectRequest, SkillSourceDisconnectResult } from '@paperclipai/shared'; import { api, ApiError, requestResponse } from './client'; const base = (companyId: string) => `/companies/${companyId}/skill-sources`; export const skillSourcesApi = { list: (companyId: string) => api.get(base(companyId)), + projectRepositories: (companyId: string) => api.get(`${base(companyId)}/project-repositories`), + adoptProject: (companyId: string, workspaceId: string) => api.post<{ bindingId: string }>(`${base(companyId)}/adopt-project`, { workspaceId }), + settings: (companyId: string, id: string, input: SkillSourceSettingsRequest) => api.patch(`${base(companyId)}/${id}/settings`, input), repositories: (companyId: string) => api.get(`${base(companyId)}/repositories`), discover: (companyId: string, input: SkillSourceDiscoveryRequest) => api.post(`${base(companyId)}/discover`, input), discoverStream: async (companyId: string, input: SkillSourceDiscoveryRequest, onProgress: (event: SkillSourceScanUpdate) => void, signal?: AbortSignal): Promise => { diff --git a/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx b/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx new file mode 100644 index 00000000000..52d426b233d --- /dev/null +++ b/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx @@ -0,0 +1,27 @@ +import type { ProjectSkillRepository } from '@paperclipai/shared'; +import { GithubIcon } from '@/components/icons/github-icon'; +import { Button } from '@/components/ui/button'; +import { Link } from '@/lib/router'; + +export function ProjectSkillRepositoryRow({ repository, pending, onImport }: { + repository: ProjectSkillRepository; + pending?: boolean; + onImport: () => void; +}) { + const importing = pending || repository.status === 'pending'; + return
+
+ +
+ + {repository.repositoryUrl.replace('https://github.com/', '')} + +

{repository.projectName} · {repository.trackingRef === 'HEAD' ? 'Default branch' : repository.trackingRef}{repository.projectVisibility === 'private' ? ' · Project access' : ''}

+
+ +
+ {repository.lastError &&

{repository.lastError} Manage GitHub connection

} +
; +} diff --git a/ui/src/components/skill-studio/SkillSourceConnectionRepair.tsx b/ui/src/components/skill-studio/SkillSourceConnectionRepair.tsx new file mode 100644 index 00000000000..9de630b617d --- /dev/null +++ b/ui/src/components/skill-studio/SkillSourceConnectionRepair.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { SkillSource } from '@paperclipai/shared'; +import { skillSourcesApi } from '@/api/skillSources'; +import { ApiError } from '@/api/client'; +import { queryKeys } from '@/lib/queryKeys'; +import { Link } from '@/lib/router'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { RadioCardGroup } from '@/components/ui/radio-card'; + +export function SkillSourceConnectionRepair({ companyId, source, onClose, onSaved, onReturnFocus }: { + companyId: string; source: SkillSource; onClose: () => void; onSaved: () => Promise; + onReturnFocus?: () => void; +}) { + const [connectionId, setConnectionId] = useState(''); + const client = useQueryClient(); + const query = useQuery({ queryKey: queryKeys.skillSources.repositories(companyId), queryFn: () => skillSourcesApi.repositories(companyId), refetchOnMount: 'always', retry: false }); + const save = useMutation({ mutationFn: () => skillSourcesApi.settings(companyId, source.id, { revision: source.revision, connectionId }), + onError: async error => { + if (error instanceof ApiError && error.status === 409) { + await client.invalidateQueries({ queryKey: queryKeys.skillSources.all(companyId) }); + } + }, + onSuccess: async () => { await onSaved(); onClose(); } }); + const connections = query.data?.connections ?? []; + return { if (!open && !save.isPending) onClose(); }}> + { if (onReturnFocus) { event.preventDefault(); onReturnFocus(); } }}> + + Choose GitHub connection + Automatic updates will use your access to this connection. Installed skills and selections stay in place. + +
+ {query.isPending &&

Loading connections…

} + {query.error &&

{query.error.message}

} + ({ value: connection.id, title: connection.name }))} /> + {query.isSuccess && connections.length === 0 &&

Connect GitHub to continue.

} + + {save.error &&

{save.error instanceof ApiError && save.error.status === 409 + ? 'This source was updated. Your connection is still selected. Save again to apply it.' : save.error.message}

} +
+
+ + +
+
+
; +} diff --git a/ui/src/components/skill-studio/SkillSourceSyncControls.tsx b/ui/src/components/skill-studio/SkillSourceSyncControls.tsx new file mode 100644 index 00000000000..7dd19bb8bbb --- /dev/null +++ b/ui/src/components/skill-studio/SkillSourceSyncControls.tsx @@ -0,0 +1,23 @@ +import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ToggleSwitch } from '@/components/ui/toggle-switch'; + +export function SkillSourceSyncControls({ automaticUpdates, refreshing, disabled, onRefresh, onAutomaticUpdates, showPreference = true }: { + automaticUpdates: boolean; + showPreference?: boolean; + refreshing?: boolean; + disabled?: boolean; + onRefresh: () => void; + onAutomaticUpdates: (enabled: boolean) => void; +}) { + return
+ {showPreference && } + +
; +} diff --git a/ui/src/context/LiveUpdatesProvider.test.ts b/ui/src/context/LiveUpdatesProvider.test.ts index 49ef0cf07a6..b0848f92941 100644 --- a/ui/src/context/LiveUpdatesProvider.test.ts +++ b/ui/src/context/LiveUpdatesProvider.test.ts @@ -16,6 +16,17 @@ import { __liveUpdatesTestUtils } from "./LiveUpdatesProvider"; import { queryKeys } from "../lib/queryKeys"; describe("LiveUpdatesProvider issue invalidation", () => { + it('refreshes sources, legacy repositories and installed skills after a background import', () => { + const client = new QueryClient(); + const invalidate = vi.spyOn(client, 'invalidateQueries'); + __liveUpdatesTestUtils.invalidateActivityQueries(client, 'company-1', { + entityType: 'company_skill_source', entityId: 'source-1', action: 'company.skill_source_refreshed', + }, { userId: 'user-1', agentId: null }); + for (const queryKey of [queryKeys.skillSources.all('company-1'), queryKeys.skillSources.projects('company-1'), queryKeys.companySkills.list('company-1')]) { + expect(invalidate).toHaveBeenCalledWith({ queryKey }); + } + client.clear(); + }); it("refreshes personal membership state when a primary changes on another device", () => { const client = new QueryClient(); const invalidate = vi.spyOn(client, "invalidateQueries"); diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index fe26a868fb8..22a111b9d15 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -1498,6 +1498,13 @@ function invalidateActivityQueries( return; } + if (entityType === "company_skill_source") { + queryClient.invalidateQueries({ queryKey: queryKeys.skillSources.all(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.companySkills.list(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.skillSources.projects(companyId) }); + return; + } + if (entityType === "company_skill") { const sourceIssueId = readString((payload.details as Record | undefined)?.sourceIssueId); if (sourceIssueId) queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(sourceIssueId) }); diff --git a/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx b/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx index a03c0f87beb..406eb0fcf63 100644 --- a/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx +++ b/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx @@ -628,6 +628,15 @@ describe("AI repair inside the card", () => { expect(document.body.textContent).toContain("Dotta must reconnect My Codex account"); expect(document.querySelector('[data-testid="shared-ai-credentials"]')).toBeNull(); }); + it("shows a withdrawn AI request as terminal without another setup action", () => { + renderBody({ ...interaction, status: "cancelled", resolvedAt: new Date() }); + expect(document.body.textContent).toContain("Connection request withdrawn"); + expect(document.body.textContent).not.toContain("This task can’t run"); + expect(button("Fix connection")).toBeUndefined(); + expect(document.querySelector('[data-testid="connection-intent-terminal"]')).not.toBeNull(); + expect(setupOptionsMock).not.toHaveBeenCalled(); + expect(credentialRender).not.toHaveBeenCalled(); + }); it("does not promise to run without credentials when declined", () => { renderBody({ ...interaction, status: "rejected" }); expect(document.body.textContent).toContain("The task still needs a working AI connection"); diff --git a/ui/src/features/connections/ConnectionIntentInteractionBody.tsx b/ui/src/features/connections/ConnectionIntentInteractionBody.tsx index dafa20679e9..48a56139bed 100644 --- a/ui/src/features/connections/ConnectionIntentInteractionBody.tsx +++ b/ui/src/features/connections/ConnectionIntentInteractionBody.tsx @@ -246,6 +246,12 @@ export function ConnectionIntentInteractionBody({ title: accessRequest ? `${serviceName} access granted` : interaction.payload.upstreamService ? "External provider connected" : `${serviceName} connected`, body: accessRequest ? null : interaction.payload.upstreamService ? `${interaction.payload.requestingAgentName} can now verify and authorize ${interaction.payload.upstreamService.name} through this provider. The app is not yet verified.` : isAi ? "This agent can now use the connection." : `${interaction.payload.requestingAgentName} can use this connection on the continuation run.`, } + : interaction.status === "cancelled" + ? { + icon: XCircle, + title: "Connection request withdrawn", + body: "This request is no longer active.", + } : interaction.status === "rejected" ? { icon: XCircle, diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index 81b1ac98387..9db6f075e06 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -1,5 +1,6 @@ export const queryKeys = { skillSources: { + projects: (companyId: string) => ['project-skill-repositories', companyId] as const, preview: (companyId: string, repositoryUrl: string, connectionId: string | null, commitSha: string | null, skillPath: string, filePath: string) => ['skill-sources', companyId, 'preview', repositoryUrl, connectionId, commitSha, skillPath, filePath] as const, all: (companyId: string) => ["skill-sources", companyId] as const, repositories: (companyId: string) => ["skill-source-repositories", companyId] as const, diff --git a/ui/src/pages/SkillSources.test.tsx b/ui/src/pages/SkillSources.test.tsx index 7ac4ccda3d7..ac8f5b81e72 100644 --- a/ui/src/pages/SkillSources.test.tsx +++ b/ui/src/pages/SkillSources.test.tsx @@ -16,7 +16,7 @@ vi.mock('@/lib/router', () => ({ useNavigate: () => context.navigate, useParams: () => ({ sourceId: context.sourceId }), })); -vi.mock('@/api/skillSources', () => ({ skillSourcesApi: { list: vi.fn(), repositories: vi.fn(), discoverStream: vi.fn(), create: vi.fn(), select: vi.fn(), refresh: vi.fn(), disconnect: vi.fn() } })); +vi.mock('@/api/skillSources', () => ({ skillSourcesApi: { list: vi.fn(), projectRepositories: vi.fn(), adoptProject: vi.fn(), settings: vi.fn(), repositories: vi.fn(), discoverStream: vi.fn(), create: vi.fn(), select: vi.fn(), refresh: vi.fn(), disconnect: vi.fn() } })); const repos = { connections: [{ id: 'personal', name: 'Personal' }, { id: 'shared', name: 'Engineering' }], @@ -50,6 +50,7 @@ beforeEach(() => { vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }); vi.spyOn(HTMLElement.prototype, 'scrollIntoView').mockImplementation(() => {}); vi.mocked(skillSourcesApi.list).mockResolvedValue([]); + vi.mocked(skillSourcesApi.projectRepositories).mockResolvedValue([]); vi.mocked(skillSourcesApi.repositories).mockResolvedValue(structuredClone(repos)); vi.mocked(skillSourcesApi.discoverStream).mockResolvedValue(discovery); context.sourceId = 'new'; @@ -308,7 +309,7 @@ describe('GitHub skill source import', () => { expect(button('Disconnect source').disabled).toBe(false); expect(document.querySelector('section[aria-label="acme/team-skills"]')).not.toBeNull(); const other = [...document.querySelectorAll('[role="radio"]')].find(el => el.textContent?.startsWith('Delete skills'))!; - await act(async () => other.click()); + await act(async () => other.click()); await flush(); expect(document.querySelector('[role="dialog"] [role="alert"]')).toBeNull(); }); diff --git a/ui/src/pages/SkillSources.tsx b/ui/src/pages/SkillSources.tsx index bf6746e91c3..05c164e9a40 100644 --- a/ui/src/pages/SkillSources.tsx +++ b/ui/src/pages/SkillSources.tsx @@ -1,3 +1,6 @@ +import { ProjectSkillRepositoryRow } from "@/components/skill-studio/ProjectSkillRepositoryRow"; +import { SkillSourceConnectionRepair } from "@/components/skill-studio/SkillSourceConnectionRepair"; +import { SkillSourceSyncControls } from "@/components/skill-studio/SkillSourceSyncControls"; import { GithubIcon } from "@/components/icons/github-icon"; import { useEffect, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -15,7 +18,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { RadioCardGroup } from '@/components/ui/radio-card'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { SkillImportProgress, type FoundSkill } from './skills/SkillImportProgress'; import { SkillPackagePreview } from './skills/SkillPackagePreview'; import { SkillSourceTree, type SkillTreeCandidate } from './skills/SkillSourceTree'; @@ -26,22 +29,31 @@ import { consumeSkillSourceReturn, rememberSkillSourceReturn } from '@/lib/skill const sourceKey = (companyId: string) => queryKeys.skillSources.all(companyId); export function SkillSources() { + const sourceActionButtons = useRef>({}); const { selectedCompanyId, selectedCompany } = useCompany(); const { sourceId } = useParams<{ sourceId: string }>(); const navigate = useNavigate(); const client = useQueryClient(); const { setBreadcrumbs } = useBreadcrumbs(); + const [results, setResults] = useState>({}); + const [repairSourceId, setRepairSourceId] = useState(null); const [disconnectSource, setDisconnectSource] = useState(null); - const [results, setResults] = useState>({}); const companyId = selectedCompanyId ?? ''; useEffect(() => { setBreadcrumbs([{ label: 'Skills', href: '/skills' }, { label: 'Sources' }]); }, [setBreadcrumbs]); - const query = useQuery({ queryKey: sourceKey(companyId), queryFn: () => skillSourcesApi.list(companyId), enabled: Boolean(companyId) }); + const query = useQuery({ queryKey: sourceKey(companyId), queryFn: () => skillSourcesApi.list(companyId), enabled: Boolean(companyId), refetchInterval: 5000 }); + const repairSource = query.data?.find(source => source.id === repairSourceId); + const projectRepositories = useQuery({ queryKey: queryKeys.skillSources.projects(companyId), queryFn: () => skillSourcesApi.projectRepositories(companyId), enabled: Boolean(companyId), refetchInterval: 5000 }); async function invalidate() { - await Promise.all([client.invalidateQueries({ queryKey: sourceKey(companyId) }), client.invalidateQueries({ queryKey: queryKeys.companySkills.list(companyId) })]); + await Promise.all([client.invalidateQueries({ queryKey: queryKeys.skillSources.projects(companyId) }), client.invalidateQueries({ queryKey: sourceKey(companyId) }), client.invalidateQueries({ queryKey: queryKeys.companySkills.list(companyId) })]); } const refresh = useMutation({ mutationFn: (id: string) => skillSourcesApi.refresh(companyId, id), onSuccess: async result => { - setResults(prev => ({ ...prev, [result.source.id]: '' })); await invalidate(); - }, onError: (error, id) => { setResults(prev => ({ ...prev, [id]: error.message })); void invalidate(); } }); + setResults(prev => ({ ...prev, [result.source.id]: null })); await invalidate(); + }, onError: (error, id) => { setResults(prev => ({ ...prev, [id]: { message: error.message, failedAt: Date.now(), operation: 'refresh' } })); void invalidate(); } }); + const adopt = useMutation({ mutationFn: (workspaceId: string) => skillSourcesApi.adoptProject(companyId, workspaceId), onSuccess: invalidate }); + const settings = useMutation({ mutationFn: ({ source, enabled }: { source: SkillSource; enabled: boolean }) => skillSourcesApi.settings(companyId, source.id, { revision: source.revision, automaticUpdates: enabled }), + onSuccess: async (_result, variables) => { setResults(prev => ({ ...prev, [variables.source.id]: null })); await invalidate(); }, + onError: (error, variables) => setResults(prev => ({ ...prev, [variables.source.id]: { message: error.message, failedAt: Date.now(), operation: 'settings' } })) }); + const availableProjects = Array.isArray(projectRepositories.data) ? projectRepositories.data.filter(repo => !repo.sourceId) : []; const disconnect = useMutation({ mutationFn: ({ source, skills }: { source: SkillSource; skills: SkillSourceDisconnectRequest['skills'] }) => skillSourcesApi.disconnect(companyId, source.id, { revision: source.revision, skills }), @@ -64,32 +76,52 @@ export function SkillSources() { if (!companyId) return

Select a company to manage skill sources.

; return
-

Skill sources

Import skills from GitHub and refresh them when you’re ready.

+

Skill sources

Skills from your repositories stay up to date automatically.

Installed skills {query.isPending &&

Loading sources…

} {query.error &&

{query.error.message}

} - {query.data?.length === 0 &&

No repositories added yet. Import your skills to make them available in {selectedCompany?.name ?? 'this company'}.

} + {query.data?.length === 0 && availableProjects.length === 0 &&

No repositories added yet. Import your skills to make them available in {selectedCompany?.name ?? 'this company'}.

} + {projectRepositories.error &&

{projectRepositories.error.message}

} + {adopt.error &&

{adopt.error.message}

} + {availableProjects.length > 0 &&
+ {availableProjects.map(repository => adopt.mutate(repository.workspaceId)} />)} +
}
{query.data?.map(source => { const installed = source.entries.filter(entry => entry.skillId); const newCount = source.entries.filter(entry => entry.selection === 'new' && entry.present).length; + const result = results[source.id]; + const localError = result && result.message !== source.lastError + && (result.operation === 'settings' || !source.lastSuccessAt || new Date(source.lastSuccessAt).getTime() < result.failedAt) + ? result.message : null; return
-
-
+
+
{source.fullName} +

{source.trackingRef === 'HEAD' ? 'Default branch' : source.trackingRef}{source.lastSuccessAt ? ` · Updated ${timeAgo(source.lastSuccessAt)}` : ''}{source.accessScope === 'projects' ? ' · Project access' : ''}

+
{source.projects?.map(project => {project.name})}
+ {source.enabled && source.automaticUpdates === false &&

Automatic updates paused

} + {source.trackingActive === false &&

Repository tracking stopped · installed skills kept

} {!source.enabled &&

Disconnected

} {refresh.isPending && refresh.variables === source.id &&

Refreshing…

} {newCount > 0 && {newCount} new {newCount === 1 ? 'skill' : 'skills'} available}
+ refresh.mutate(source.id)} + onAutomaticUpdates={enabled => settings.mutate({ source, enabled })} /> - + - refresh.mutate(source.id)}>{refresh.isPending && refresh.variables === source.id ? 'Refreshing…' : 'Refresh'} + settings.mutate({ source, enabled })}>Automatic updates navigate(`/skills/sources/${source.id}`)}>Select skills + setRepairSourceId(source.id)}>Choose connection { disconnect.reset(); reloadRemoval.reset(); setDisconnectSource(source); }}>{source.enabled ? 'Disconnect source' : 'Remove source'} @@ -100,24 +132,32 @@ export function SkillSources() { {entry.name} {entry.description && {entry.description}} + {!entry.present && Removed from source · installed copy kept} )} } - {results[source.id] &&

{results[source.id]}

} + {installed.length === 0 && source.entries.length === 0 && source.syncState !== 'refreshing' && !source.lastError &&

+ No skills found yet.{source.enabled && source.automaticUpdates && source.importNewSkills ? ' New skills will be imported automatically.' : ''} +

} + {localError &&

{localError}

} entry.selection === 'selected'), source.repositoryPackage)} /> {source.lastError && source.lastAttemptAt &&

Last attempt {timeAgo(source.lastAttemptAt)}

} {source.lastError &&

{source.lastError} Review source

} + {source.lastError && } {source.connectionId && Manage GitHub connection}
; })}
+ {repairSource && setRepairSourceId(null)} onReturnFocus={() => sourceActionButtons.current[repairSource.id]?.focus()} onSaved={async () => { + setResults(prev => ({ ...prev, [repairSource.id]: null })); await invalidate(); + }} />} {disconnectSource && reloadRemoval.mutate()} onChoiceChange={() => { if (!staleRemoval) disconnect.reset(); }} onClose={() => setDisconnectSource(null)} onConfirm={skills => disconnect.mutate({ source: disconnectSource, skills })} />} {sourceId && (sourceId === 'new' || activeSource) && navigate('/skills/sources')} onSaved={async result => { - setResults(prev => ({ ...prev, [result.source.id]: '' })); await invalidate(); navigate('/skills/sources'); + setResults(prev => ({ ...prev, [result.source.id]: null })); await invalidate(); navigate('/skills/sources'); }} />} {sourceId && sourceId !== 'new' && query.isSuccess && !activeSource &&

Source not found.

}
; @@ -139,7 +179,7 @@ function DisconnectSourceDialog({ source, pending, error, stale, onReload, onClo {count > 0 && { setChoice(value as SkillSourceDisconnectRequest['skills']); onChoiceChange(); }} disabled={pending} options={[ - { value: 'copy', title: 'Keep editable copies', description: 'Move them to My Skills. Assigned agents keep using them.' }, + { value: 'copy', title: 'Keep editable copies', description: source.accessScope === 'projects' ? 'Move them to My Skills, keeping project access restrictions. Assigned agents keep using them.' : 'Move them to My Skills. Assigned agents keep using them.' }, { value: 'delete', title: 'Delete skills', description: 'Remove them from the library. Detach skills used by agents first.' }, ]} />} diff --git a/ui/src/pages/apps/chat/ChatExecutionDefaults.test.tsx b/ui/src/pages/apps/chat/ChatExecutionDefaults.test.tsx index 2d6f2048117..aafe63c4dd4 100644 --- a/ui/src/pages/apps/chat/ChatExecutionDefaults.test.tsx +++ b/ui/src/pages/apps/chat/ChatExecutionDefaults.test.tsx @@ -4,6 +4,7 @@ import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { ChatExecutionDefaults } from "./ChatExecutionDefaults"; +import type { ChatExecutionDefaults as Defaults } from "@paperclipai/shared"; const mocks = vi.hoisted(() => ({ projects: vi.fn(), workspaces: vi.fn(), save: vi.fn() })); vi.mock("@/api/projects", () => ({ projectsApi: { list: mocks.projects } })); vi.mock("@/api/execution-workspaces", () => ({ executionWorkspacesApi: { listSummaries: mocks.workspaces } })); @@ -14,8 +15,8 @@ beforeEach(() => { client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); }); afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); }); -async function render() { - await act(async () => root.render()); +async function render(value: Defaults | null = { projectId: null }) { + await act(async () => root.render()); await vi.waitFor(() => expect(container.querySelectorAll("select")[1].disabled).toBe(false)); } async function choose(value: string) { @@ -39,6 +40,19 @@ it("keeps edits visible after a save failure and permits retry", async () => { expect(container.textContent).toContain("Saved for new tasks."); }); +it("retains accepted selections until the parent refreshes and then follows later defaults", async () => { + await render(); + await choose("task"); + await save(); + expect(container.querySelectorAll("select")[1].value).toBe("task"); + expect(container.querySelector('button[type="submit"]')!.disabled).toBe(true); + expect(container.textContent).toContain("Saved for new tasks."); + await render({ projectId: null, workspace: { kind: "task_directory" } }); + await render({ projectId: null, workspace: null }); + expect(container.querySelectorAll("select")[1].value).toBe("clear"); + expect(mocks.save).toHaveBeenCalledTimes(1); +}); + it("offers and saves an idle workspace from the shared task selection filter", async () => { mocks.workspaces.mockResolvedValue([ { id: "active-workspace", name: "Active files", status: "active", closedAt: null }, diff --git a/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx b/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx index cc60f61571c..7e3ccd4ad7b 100644 --- a/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx +++ b/ui/src/pages/apps/chat/ChatExecutionDefaults.tsx @@ -1,4 +1,4 @@ -import { useId, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { ChatExecutionDefaults as Defaults, TaskWorkspaceSelection } from "@paperclipai/shared"; import { projectsApi } from "@/api/projects"; @@ -25,13 +25,19 @@ export function ChatExecutionDefaults({ companyId, value, resource = false, onSa const [draft, setDraft] = useState(null); const [pending, setPending] = useState(false); const [error, setError] = useState(null); - const [saved, setSaved] = useState(false); + const [savedFrom, setSavedFrom] = useState(null); const projects = useQuery({ queryKey: queryKeys.projects.list(companyId), queryFn: () => projectsApi.list(companyId) }); const workspaces = useQuery({ queryKey: ["channel-workspace-choices", companyId], queryFn: () => executionWorkspacesApi.listSummaries(companyId, { selectableForTask: true }), }); const current = draft ?? value ?? {}; + const valueKey = JSON.stringify(value ?? {}); + const saved = savedFrom !== null; + // Keep the accepted selections until the parent publishes refreshed defaults. + useEffect(() => { + if (savedFrom !== null && savedFrom !== valueKey) setDraft(null); + }, [savedFrom, valueKey]); const projectValue = current.projectId === undefined ? "inherit" : current.projectId ?? "none"; const workspaceValue = selectionKey(current.workspace); const choices = new Map(); @@ -46,13 +52,13 @@ export function ChatExecutionDefaults({ companyId, value, resource = false, onSa for (const workspace of workspaces.data ?? []) { choices.set(`existing:${workspace.id}`, { kind: "existing", workspaceId: workspace.id }); } - const dirty = JSON.stringify(current) !== JSON.stringify(value ?? {}); - const change = (next: Defaults) => { setDraft(next); setSaved(false); setError(null); }; + const dirty = !saved && JSON.stringify(current) !== valueKey; + const change = (next: Defaults) => { setDraft(next); setSavedFrom(null); setError(null); }; return
{ event.preventDefault(); if (pending || !dirty) return; - setPending(true); setError(null); setSaved(false); - try { await onSave(Object.keys(current).length ? current : null); setDraft(null); setSaved(true); } + setPending(true); setError(null); setSavedFrom(null); + try { await onSave(Object.keys(current).length ? current : null); setSavedFrom(valueKey); } catch (cause) { setError(cause instanceof Error ? cause.message : "Couldn’t save task defaults. Try again."); } finally { setPending(false); } }}> diff --git a/ui/storybook/fixtures/githubSkillSources.ts b/ui/storybook/fixtures/githubSkillSources.ts index 3367d5a72c0..cbce91494a7 100644 --- a/ui/storybook/fixtures/githubSkillSources.ts +++ b/ui/storybook/fixtures/githubSkillSources.ts @@ -181,6 +181,13 @@ export function installFixtures(empty: boolean, needsConnection: boolean, option return structuredClone({ source, imported, updated: [], unchanged, warnings }); } skillSourcesApi.list = async () => structuredClone(sources); + skillSourcesApi.projectRepositories = async () => []; + skillSourcesApi.settings = async (_companyId, id, input) => { + const source = sources.find(item => item.id === id)!; + Object.assign(source, { automaticUpdates: input.automaticUpdates ?? source.automaticUpdates ?? true, + importNewSkills: input.importNewSkills ?? source.importNewSkills ?? false, revision: source.revision + 1 }); + return structuredClone(source); + }; const repositoryScenario = options.repositories ?? 'single'; skillSourcesApi.repositories = async () => { if (repositoryScenario === 'loading') return new Promise(() => {}); @@ -262,7 +269,7 @@ export function installFixtures(empty: boolean, needsConnection: boolean, option if (options.saving) await wait(3600000); const source = sourceFixture(); Object.assign(source, { id: `source-${sources.length + 1}`, packageMode: input.packageMode, repositoryPackage: input.packageMode === 'repository' ? repositoryFixture(options.packageScenario) : null, repositoryUrl: input.repositoryUrl, fullName: input.repositoryUrl.replace("https://github.com/", ""), trackingRef: input.trackingRef || "main", connectionId: input.connectionId ?? null }); - source.entries = repositoryCandidates(options.packageScenario).filter(entry => discoveryCandidates.some(candidate => candidate.path === entry.path)).map((entry, index) => ({ ...entry, id: `entry-${index}`, sourceId: source.id, present: true, selection: 'new', skillId: null })); + source.entries = repositoryCandidates(options.packageScenario).filter(entry => discoveryCandidates.some(candidate => candidate.path === entry.path)).map((entry, index) => ({ ...entry, id: `entry-${sources.length * 100 + index}`, sourceId: source.id, present: true, selection: 'new', skillId: null })); sources.push(source); return save(source, input.selectedPaths, input.excludedFolders ?? []); }; diff --git a/ui/storybook/public/project-skills-evidence/automatic-attachment.png b/ui/storybook/public/project-skills-evidence/automatic-attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..65a1b92b8ef9668417baa7bda29c18b874168495 GIT binary patch literal 69491 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 literal 0 HcmV?d00001 diff --git a/ui/storybook/public/project-skills-evidence/execution-master-final-marker.txt b/ui/storybook/public/project-skills-evidence/execution-master-final-marker.txt new file mode 100644 index 00000000000..9b89f2ebd60 --- /dev/null +++ b/ui/storybook/public/project-skills-evidence/execution-master-final-marker.txt @@ -0,0 +1 @@ +project-skill-v2 diff --git a/ui/storybook/public/project-skills-evidence/execution-review-cli-marker.txt b/ui/storybook/public/project-skills-evidence/execution-review-cli-marker.txt new file mode 100644 index 00000000000..9b89f2ebd60 --- /dev/null +++ b/ui/storybook/public/project-skills-evidence/execution-review-cli-marker.txt @@ -0,0 +1 @@ +project-skill-v2 diff --git a/ui/storybook/public/project-skills-evidence/execution-v1-marker.txt b/ui/storybook/public/project-skills-evidence/execution-v1-marker.txt new file mode 100644 index 00000000000..b911a57c1f3 --- /dev/null +++ b/ui/storybook/public/project-skills-evidence/execution-v1-marker.txt @@ -0,0 +1 @@ +project-skill-v1 diff --git a/ui/storybook/public/project-skills-evidence/execution-v2-marker.txt b/ui/storybook/public/project-skills-evidence/execution-v2-marker.txt new file mode 100644 index 00000000000..9b89f2ebd60 --- /dev/null +++ b/ui/storybook/public/project-skills-evidence/execution-v2-marker.txt @@ -0,0 +1 @@ +project-skill-v2 diff --git a/ui/storybook/public/project-skills-evidence/index.html b/ui/storybook/public/project-skills-evidence/index.html new file mode 100644 index 00000000000..5744dbea24c --- /dev/null +++ b/ui/storybook/public/project-skills-evidence/index.html @@ -0,0 +1 @@ +Project skills acceptance evidence

Project skills acceptance evidence

These captures and receipts use disposable integration data. The Sources and execution captures are from the real isolated app. The repair-dialog capture uses the shipped component with Storybook fixtures. The full acceptance report and current qualification status are in the pull request.

PR #16066 · Execution and version receipts

Real Sources page

Project repositories and installed skills in the real Sources page

Mobile connection repair

Mobile GitHub connection repair dialog with reachable Cancel and Save controls

Mobile repair after a concurrent update

Shipped components with Storybook fixtures. The selected account remains checked and Save stays reachable.

Mobile repair conflict with selected account and reachable retry

Real routine execution

Real runner confirms writing the updated project skill marker

Post-review routine execution

The real ACP routine reports the pinned v2 skill result

Supported CLI execution

The pinned Codex 0.160.0 client verified the existing v2 file and completed the task. The earlier unsupported-client attempts are retained in the receipts.

The supported CLI verifies the v2 marker and completes the routine task

Final repository attachment

The real board UI attached the disposable repository to a new project. The background worker imported both skills from the default branch without a Skills setup or Refresh action.

Final project repository attachment automatically imports both skills
\ No newline at end of file diff --git a/ui/storybook/public/project-skills-evidence/repair-conflict-mobile.jpg b/ui/storybook/public/project-skills-evidence/repair-conflict-mobile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d78e96f2f506bfc1e4f66be29741c38e4b6729d3 GIT binary patch literal 34491 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 literal 0 HcmV?d00001 diff --git a/ui/storybook/public/project-skills-evidence/routine-execution-cli.jpg b/ui/storybook/public/project-skills-evidence/routine-execution-cli.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8c0385c3c54ff167bed38d3f3ce0dfa97dce4d0 GIT binary patch literal 93748 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 O7B(S*2S3aH|4jg%_vbAD literal 0 HcmV?d00001 diff --git a/ui/storybook/public/project-skills-evidence/routine-execution-final.jpg b/ui/storybook/public/project-skills-evidence/routine-execution-final.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3712144684fb357c57b52e3406400dccb7254bb8 GIT binary patch literal 89112 zcmeFY1yEdHw&CI>8-rTBJ^}f?pySn?FUVH7;Ywu;h^)&mm3c&d&{XrUlfB*m>Jih=>3jj#~ z@=GLSq?gFZNXRHC$f&PyUcEv?d-Vnj8v}>%4G|IH8v+6nGCB$pAPp%20VNA14Lt+X zTP9)(Rt{E14mw6A#=nRlprD|J=U%2>}V?|Ksba6@ZKSViXC0h(HZ^fs25M zi}2J5AbV!=1tP-p1^DOn5*ZN*C;a!*0uY`jc!`Yid{Y?rS#~d8prE{Xo)H1*FV+xV;3AH_q~erN ziphSBhfknvaAN0+^rozB4Vn5Km!#jRAq}^EYe^D#=2Ro{3mhXBksBK=47 zv!4Kj9R*ciq#El@cr=K33{|Sdc6ZYN<5!iN;(L7e2^Z$mQ8ULgKhI{aCv{d{%x8a; zQ)T7au&Wel<&Ifw!2t>ZXRj~D06T2AzV1|Qf>Q-xFiT�Bs9G@^Ms$IV*XaGw(c4 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 K#K8P(`o96M2GjBY literal 0 HcmV?d00001 diff --git a/ui/storybook/public/project-skills-evidence/routine-execution-reviewed.jpg b/ui/storybook/public/project-skills-evidence/routine-execution-reviewed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a783a7193a30e68ba4c7b89559eba0f0efac62dc GIT binary patch literal 93326 zcmb@t1yox>yEYn%wWU}o?p}%&mmsCMw@@rdaSKws5TrnHhvHha#R&utl;ZC01Sd#v z=kgs}_x$HO>%Zr&v$NK$o!PTzCOhvt`+1-DnTMH&6##*foT3~64GjQ5d;9<%<^j?G zEDTI6Obje6Oe}0{tf$WjoO;<9{vCbpFSDE1fZia0G<${ zp%bD#bO5LyC;0>&?ePHo`+j)`=xCT2PchIjFwn{W(HsMl z@Ff;K578?IsgEYuyc$l#jQr02aX+g__}-f`#g7WOK*yd+e+nRFW)aj3%xyc8)pGlu zQ2iGrBqL{LZt41?W_9hcbIQj-|MSoQw8ze|FtHym0tp|V-;*bp*e{+uK?k7!V{&Lu z2+`>=Uh+tN#58gGIl6j8^op2-!PLw@F1Mp1817c@ruZ>r-pR=P?-Z2Vq2KD9 z$mYqFP2t8uWewf!@&jeeo;SP^6AmAOf%d0&cP|0sz0@JypGO`5IIH7jpJW}&(G@TpWYUfh zC}-`dW#`4z*hvzMYo&%G%_|kQ&?;9e-yI&-4Y$d4j?FY{Wwmy$l`Xqe33~_*RCkgT zrItI={n>okW#>+ZyjrRWjLyA#Hd$|HAc+`Orc}%de)7G_$vzpUDn;UN9|b zB^*hr2xN^+=J6I3&RY1P8V=GV1;<{f3)iUC?jz4H_px8gj{5dDveHhRv1YSkIm<^mlrqBXp(TWWg@5Ca_pn{BmD02x?0#=B)ih<#yE!QE$Bd86V!u{^Um6LKTv_O!MxMXvDT2hgb=?FV$5-rW``B879UNdI|Fz%*kL^G2>PrHK4JJ2bc^Ig=pznSfv*ZY+8eYRaDm-#E`J_q*;3Vi1fWpsn6uk8}?G z?mUw$#cvF|<4STAQ-Jh+_YLQ*8|YGjhp1T_A&&FWsfsB>ot0PBE<1FT!r-F&2gYZq zQH*cCA!~&$r|j*ZsOw0#ujQ~IVsoZOaq5@Pj{kZSEmu*W)e!s&cf=trl2hi9$4FWR zg8y0}{_;vOFI_ZM=b(v)_^lEN2Jx^@6mH9CD`PB_Il!l-1nTL|I!1YNx}?8xa5-8% z!9NlmJ+8j{F&(@-r=zL^d*RZ?e3^E+ZtY@#6WoSf^_(p|51+`0`IGF#39+Ho^W8O4W?k?*SD4=FGNZ!ml`J@n zt(ppjs>4|0F1&HK_3|=vx1p!|O1fhENWF zmB0vMoXEB$KAKr*)(~CS448h ztO5L^d54fCcXzoE^H&o(3(1OR?5-*zpwhm@xJk3rixN>buq=D%#*0XrTW@!HR+@Ep zhtM}V`!4x=u6OBrz&Hu)b#v=3?b4}Q!KzXd$ol@z7HJge5Bf z1c5&19tdX01Vdmc^wZYHP6cmo+vlL28l>k{5xc;ZN#0CA-|v|!l!$ys+s7;#!<)G& zxCME8Nki1C4GLslL5@haRTkcq85s87zLH z9G558sXVS~cp@zxN^L|$=RRB5P`>}OFl&hN=!0{OAx?qUXY}j{dfR;R^!LCQ(Hw62 z1nG96bq*;3%TpKool&$JK;naB73qYI({(=mEDEDSHJgQ9RIS2l`BcwIjtFT|O_WFn zdYQGO$xxiD9jp9`SMU(S5O0@KE$?9J?^%lwp5u=5_ZHx^%^Ga}m|%Yj#+J4ZK2jm= z(aAs4|47`J#!u7~1@D+;&`j~H?sEdHLoUVJXq288b48qcjPf@omGL;&noDd{SlIV0 zzYxe-Ccs->GOP12Q&!k7i_a@I{vtdePMN96r8wN9&i_%+-Lp2RVS%*_1L0-f9I#{UuEW7S&;MX?9s!R zaclp5n;)d;J&ry>EvGUBKwxsV_z#bdu7%IvWGW0$ zU9lRqE!o@P=C)G@i^|K_enYdtL+=G*`!v&SZI}CK1?>#TW<-;~BJ)mWu4Fxwj@pen z%D&h|f#2a7V<%-O*Hxg~`W3ef7Mq|vlVBH&tzpJ4NOgV|9CPtfh?()lj1*H;Lgjen zSYT3a{LUQDI&^d@H9-x^>aC;Q4R-} zer1EN1g-!H>^L~xuiTn;|L$4@d%xH?j?l7PbCpPrPz~=|%~y%~ zzoIlBu<`=Mh{2jGfRd?fYW3Jb)IoGrk{-QvyRMP_0g&d!x`q>aO@pZ`YG?S;gu9i^ z{_pr^)QB=;?N`{+3FKz*Na*Ox>(}RWX*C%fvP|kzm+*q3&vCjF_g*%3uJ_ONUrOjH zWRSu;JHFj7_I-PNwUD#lkCGnZA5@hlJ^-vEEA{R6Y(}B>`@{qQ7_qiPiM|1Px6?W0iR2DEMmiGO;5VoD&gDbOlG7;o8-6VBK}0zUGWS*R;jBGBWS+Y+ohdS==bXlsG4m7jiBCWlG4Z@?-;3@iB~W zG${(eL7o=}EWAXgTzdd$$Jp20Qb{~-pCALl1*Gm*o5Z1dR22^CdU-N6G53%YS`1;9K@ecsi3}K1)(|`j2idEV$y5{L+Q%vcBbGGWLYPzNjtM%7#lB86c13y>V z(EWuyKcd`LVTL)UiiVTd>!D#`#ZBGvUyDRbewVwkSIOOKA%y1$&3>MOR{Ml6I-1P) zmr%15w6a(c*fZ+OyUjrZ+-eAgV6r1QO%2_^@85fljtvy)V&6@DyMHriwa;+)AsKNT z)w#xQUKlp35+rIhRY~TK^T%nac_+v2-ohncYC^PnUvf@#`ga+^=2MTcGgQVE9ix^hg&u z=Wl>p@R*#>&Yx|_*;9ZsGfpYCShr4{&V!;|Y#ahvXjJtv6(m9OG9>qDd-Io6L{6OD z2yX50nO1{^py9V_0IKvN00bX{pw8?@tuG_m7BYjBy>76Xx9p_+Aae9fe?}xPna(f9 zOr8P*Zx2{Cs&R@V@^tj6gR0$w-@oV#ts*-`!dht z(7RNzL-Etl=X0QQJ9Cz^^6>11h(wDS7^^i z@T4@E7O;QWSh`p!;xpXt?NisR&7fKY*3-|^O1`NN*EWjLmi9oKkURMqFj)M-?Da?q z&i5}Jy|Zj|@vhj|p3I%YOuq&ybozf30qxg#xO!%ys#N1ov#h>4G;P;cm5a$OO@+|h z-DFM?V&*;o^wfR7SIu^sX2&LBO(rmHXIOdL1MoT*&}&9Sfq0?u+2f!3YlEa{tY6~XJvR70Uv3&x4x_!UXvwN0UG?h14lGy*y@q&(*l zN#r5x@T`z>WV2`d5=68<`%aAHBTa%PZyCoi7hC9G5fl2xP@yV*}G)7d)`JxyJ2T^!; z-#(IfW$nkH@P$ia#uJ{E?v!6pG4Vn&Z*a4tZ(cJ=Sr5AXxbmtE%Ft$O!12$Wl8pmS zXNO^1IdFi(01X3R#mg(;Rr>A~A8{E(oAD*DV7qKaf{ACA##6DTM`vJy44Hw{eeZ_$ zo(5XcIx7jj7{y8^TaQtrt~n>6-Cj#PP{dRgn4$dJO(Sz?7B(|BB|}toCXm#>+XB;L zc^QU)r0sVL5xfXgyXg*Pg-}wCecwFn@Jl-m6|`T79}qBg(Vf}r-l^pGtFKQuV@fRj zC%mN4kHiZiWM zqZ1^+gBqGU1Ko)_<761@XH(bYethIJ{|Ee|#;n*`X*3RQSemf@=U21oI4MsdaQPDt z2DKPJ3Up|g%*&ynIS*xN|JA2O#M4XQKN3a1HU+*rdLat&gD+ZkJ-E+95_(7Blsjg0 z@Yt}*rJi=2bklLH=nUluxqc}RlBm7LLI%9Nsbw3^bWV?3NlmI%K?w@Z-n2veq;4eA zyc8o=;zID4b0aUutpF5LhNRN+O6DO0Pl2`h_m*G=E!MfCXpI|f3sPdrX$qI##Hs{p z=3ZHM^Y!}X(l0K9yBuI%ec~V zh7FqOU)|`E3hy-CRX5j%&Jl`d)#MO7!-E9ATlbmz2E5ez?^#``x|rr`qwyVB`Pqsq z-o_E$w9^?9_7A2RvzvY*WsjN%myEFib80I3nYpzUxwPMy+C_Ko$h6;^jDHx1Aj@ishO4Urz5){& zBa6Y&&&94Uv~9_|r1t4=aeUso%^P1`Ufe4$KLD^t{mwfd0QXP*Cc$D+g&p&n<-kuT z1GIN;%GnINlM=m%Dz(U=0WY-%yU9oi0tx`4E}_2?6R~=7#WpW#^5OmNzDU25giS$t z*IW)g7eueOaUrBSI_=qXX1Ujuq(ZK}mY7G^e$$*>T!=_Vy(w4gXC!F6APJtHpwkRa zT`Ati{wb|TpF(e8)=j{J9h+Nte-mb|MzWB=Ie|r4MGg|WA3sILUVHC~98&SJ-gCAV;$5=shy-KlIsxtjn?}jx8mU{Cjo8IawEO{Ue#pA(!M5_wpiGpBOLmU5(&I* zVzL%KAq{uliJnlCptA6wlAS}fnsQXV+OJM*;g)_Iu2i!PZBN0G!gD&)|G7>ZM}3;J zbtBZ(x&Pvh@5?}UL#bm`b;LLEurELMF714j;9}|d(|oVSAuipvtcvZS&HX71NIm?j zU%z{QIPTQ7&XjfP_zqx8m~164?U&O4uQXuye-QBll^OfFl$Ks=y1%fiT2dLEg?ofa z!Ig&_R@y-^Dn>SY(WJLZqfs)0xd!y4CPn8Wn2}i&c_;C53qgL?ypY!tvGZN9{?>(S zPkp!hj@?0vw$wdM?fA;-k7wJkV?C;GF6b`;mmP-Yp%7x*8eE2q!mRr3JKxdAzXWQa zO8U9)E~ZVXV0J#at`XW!l$(`q=DQAFTzE!Z=7id6^|5*P066j?eE`^fegLe5+-d)C z)=my1EsU<;*Dhm!o>Az>dE^P|(j`-K_D z`F&D=9+?k7nhBf_HoIBuj{sbPlLJvO@3_K`YOqfh<5b+;-x>Iu$e6Qs6hqHCNaaJ|auJLiD=9sG_}H|>8fuxEc}7ZeUkJ6qLK+LvCY<6Q$-HnDQ1^OaD^dBNLdxErY)~`46>ST3r9?>Ie7{dD#Y?MPOEtZXj%%u!xw|DM z>ft3(TqgYJi=&U*aCUAhM=_^RM3;$#TTu7O*!r)=!E7iIrf2JApOt{nqL17h0gT zzvd9(?Pfd3_d`EgT4GqpjX=k8-j6007kX+hwNP6zhz+C#-MpXw;XUpY)0v zXqNF?93vdWK2jLH7=&fONVTkH4Z(VTqo>Y#=o~*5ZJX>r5VTkU49F;$JWNLv+;6hx zE=#o~`uF5Lei=vb?&ybxHaEWmB>t6?dw0=<+k9~d_kqYb8zu7DtJNz|J;Ma${K}A}d$xvfqC?kGO-G24BktQax-vQq zW%VKTA!NGE&hox5J-W=+npb0v7^-PjfLl?{oVJEIq-{O4_N%#gb00Q#Ncg)hOUR;= z#x1D!UyNXi?Jn_?CGT13ln%Wb$eq#%bTndjXvGpwwcoRd*Wqi+&3FE_&x0krEb>Q{ zG$m8q6q^^9@fQj%YnK|lt}yZQmsyHqdnMXwqKFfSV}NVv>xhASgMHEuezTg?7KZlY;uQJ$M*jh@PJ5J^nka z_{(wh*L@Y6dLqH_#sV$bgnx%9|Bg>&j}2x2EzvHJg-%;RlX7=VyGtwCYy7{KbpKb9 z?rFF*c34$vBUsCogVU@aUNQfLxt;ZV#+onsRZfhog@?Sp&m4^B0_90w#A=$EWy<0Y zy~xDlL|>~Bjlt%^^BV9;JI}3tnnj+&8Zi%DjDAOLUrx!`QcEC{J1mz!eK1}0F{>cr zrQqw?N)eIncXsF0!<`PTXbCa&v3bn$f{Ra=Ia3xk1G@fUOY%J^_^n_7=OgU1Gus)A zZJ-7%C6};VUPEhH&M6PJ2(V+qG(5|b`c}b$-jQy3QElbc7Cq)Jfx&aqpkoC_L>b+8 z8GWr^deusc^`;v}IX0=c-&Uv&IFV|@e(|`U{t34Im(=S2OI5+ONb~@(P<{aT`W?Y^ zwII2E$lrd4!}la5&+oY7CI7Z>d&0SIitQ239w--41N(UarZ1zeFB^H~dt!CE%pLjY z_mK@2bq&tv{_Jqo+bc(i5z}uD*PNO|^|90;umGcS?V_VSwpq_HuoS0NJG??L`UN*n zg7c#nIGi}l!EM1&bhRFRTjGtVBBH8-6Xt~0AF0sw^4U6ed6E`SLwfY}XYBaZgUFWg z9m(0NYu^f@?xebIKXH$-i?}>9N-tXvv(wVQ2i%rzqqp4_gk(-Ugy1$ltyG>mRW`#~ zuh<^pp20n{P5eJW9P|7}g7c<4mljd;w8sI&ZWbj*V9lpsnX3Z2*e?G1STv?>?{BP^ zZ>ED(ZZri|1yszd6y2L+JBf~+L|@mn6+ms8fT9=LeByjt^w_uYej8dHT2VA9%i@M% z$MIFyrx&eb1cw()4}iWbxcK4ecgnk7$>U$ra(}u7uVU^A$5JrTsh0pw6elY|<+*Qd zG827)xSmn{X2-Fu)VQqX?AL?}S0Hf_q29Kf3&i~h4DRK3<4t?4_y9=MA!L**6jV8L zCko@OO5I7Qjt%r{V7>oDG>&b0`)^H1I>QCoCrT`>FWYR&UAbn&0u4ZZTr1H#!u(yD zI*7{YtY@u8+4sx;GIe!O^1q#Fb!cv)y1bh3*~CaB|D#sjC<}J|9@lq0`%24+Xb=&3 z=1!yWmBSateb<72^{>BC^50KOLbDvHtV$}66WYwqe*5ho`Sr5iYpq`V)}i{IHGx`< zv;q5tiAwgoZkFR+GCHv}TYEXjn5br-hAYps{1%-3S0wsZo8MqFSDej-6WFHM-VllZwZ zeNVRxxDcO$a%JkmN{5aYdM%vErOA7ZV0K;n?PBE#q1Sfj8e|7~by00Mc%pi{Gv^gH zB*rOKbE_S7btlKUsMAt&4zE{;Gv<{mVI;m=6qnQCF)s7hG9$1C*`CamIj&2;?wC4B zfa2{pjO<=3ZJJkI`5hoW=&065p^!SmniKQJfdfIm;f3$l@GHo2L_KzE2S?5E)f+8~ zGPS$Lv%&`e@9RPr&;wuqJ-13-uH#jSev*U$@`O;*Y-!E)U`a0QvdqmM^dlc)kUPG@ZJMsfAbdW(Z5(QaowSWhiVnBNU$2Q% zSsmx&LU82Vdd2y7268y=D$H-aeG?j`A2Q zILe*0$Z)Yks>7;E;(Lwy)L42AsvL}kLx$-g->&SkaX2}K)S?t^$9fhjK!zvbY#CEP zUw5=oP*P5OS!&795|PXau{R207xNrtWmntbG+SM^PRLDo#(7rreyPreq_1e(w%p zeUI(Y-S@5~?Gs>b5(&pwPch5B?xkoe3{dwV5c-K&YaTYTro<@F2=Bkz65S{YP4ImS@%zfa~cIgWZH;Rc%Mcmn(hhQ9zVX_QfPY|mkx$CjGRHf@- z%FLrxV1~Wg9d9H35y6*g`6)V&*CSo~tu#GRGf&PV<-Zi%#(2!S*{nHWzVj)mOf0JJ z%wqZX3r&$y_gf@-Zq27)d3-RnT4scA1ZLqgvq+_J)+d5gn}M|fz^JQFX-!W7mFYn5j+597rhz^~JwWz$D5=pNms=B_TlshbV_0&_ z-TnQ=7kszUu-~eOW*~PVKV@^D%*4Bv<(!^HBD1s1&QkXVe%y0xX|uLRsn!Lfy*qR= zIDClf+q0*qVL)d1-IxLm36!Nk4A;iIT5PIRQ9SiC|FLkxvBKJxa5huIdZ~5zC`lgy zr%LNxqR0K}1K?6=9+c{L^XBlb{Skj8CdJ)pxvH<)Np4YdS%yX&hZLJv+{k=xmTc^T z>fGgWFVQZV{R2nH#+`SdfJie989>rpM|CEU^_y11L{2yG+fBA}a%|OTg`JLa*?Y{X{6-BGk_0Tqz#U(=JKYsj_KDZBn!MaCycOlpM0Eo2R{$v%% zqf^WYA9T>9e7~V6b;6JBoBjY0-1EQwVh~2)E_NE*K6aj}k$F{_oo6DD|Ab^R+=%1O z%T7E+tD=!ce&c7O2EEv3v)EQm z`-ErM>iT3iQ&3)nUG3*jUGq61@SiJ$dF)ciMWUY-jNnCGr%7JVtuy-wl3y*bXI6Or zRcecU{Ihf87a=j)A*hu5cJ|Gi&o?IzfS}^PCI1Og>FjvN-ILHt5AOIaY8JRUl8o=LHVTS8bN&u% zH<|O#4F5i`&av6}5a(FQqA1HlzjCL@#vDri%r#0vI{IAd zx>zfxV_HDAcHnl?CjE3hrMpJI>wa1*-rI*>pM*A{Dl&m&Ry?3ZBqsdBwutCw&8+^w zUMKh3r;IDVmo1V@NB5qpiewVy2e{Kn?%rwk(4Jsr4`8Cvn6c7`%v>G|kxMOmHCcX% zd;lCADr-C|sNiF%U|dTKNigarX(-x!di29ulgw>(QfMl|*CorZLQz{_xA51(_9DIQ znb(g4z24cVE0w_R0*!RbwDC4adiI5PxEC#leoQ;|8maZog^siebDh|mg~9NdH_!6? zNUe3go^-g2QXWHyhu3yrbiD70VD||akJv((ASRuT4VN3P6ORia+-l0apZbgfl)1Pv zM`s`^ClB&^LJQP3+%a3)Z1ThgAyBi8#Ii`+XG;|3YxtI# z30Fk(e+R<|XLiJ7)5g3OKO$@M^QfX`Sf`yJ^C!u&oq3zjP73-5`{Mg}sNZr+ zmeCBiKTA*k3gQoqOHB-QEVNXw^FKX`}v!dRghFJ&3!CPOm(lx>ALfcV{;#u4t zQrrE4Rvn0{#Qdu02Ir1u=J=en+IR8T{-No5#E(|tzalo*YcydM7a63XAidoRllQL>j#WV^z9B)@L> zHDnoAG6&BB^IdNIE2v;7t%~>Mw#_msDof*|+RLR*o3$IlqxRL+RSxE=ZR?`pLqd!! zlGN89CmGHu0z4Pw85nK2&`J+a4u8(B%a z`4K3f5Ni!XK+d?XXL6!<#Q`S2#h+yine9EI#EO*Osb+`iN0f*NAp){|@uUupsDlFW z*NXb?%cVm`u&rf$S-E^~eslJ#pw{;dI}W0FM5Oz_My!AQer?UnIaFhBpw$#BaR<8rL%g&Yu88! zP#`1}zz;hBo@}i*bV7)cWTd0`p$QYF+}PXX6k?K9czpq(4(@{{yx;JUrqcgFK~`$ zpn{k*m0z#7LJXu|RR*qPm;C6^t>UJt@4;b|OlD$D^LK__D`5(ZV|t(-u)1RBVX$^? z#p>Ivb!hIO7r=dtNV5_zyY&}OZc0VM=k9|B3U*f3$anfp4jc)j&}A-XurKlB|7Jd^ zpB&;%zZbM2fp@=S2XWmxZfPV zNk9MWyGWCpqLHPL%}i;S0;#XXZfNcOL%Wibn$gdj??#@`JXaM!{xown3WFpT>s0U= zZJeY)0m!p>sCQ49iSO-xyxiI}|7{x$$q#G6_VTbg7$L3lme_Gv_j&HGMXn(IU(Uj} z+)BzAIm(uc$+|8xR2Ep;`{gr7?(P!6YfLygoAI6-ad=W0Vk<(3Q37qh*WbcZfxAPuyPDEnxca0uLKrn6%fahcVaR77YKLG5V$VW64?zOSS_;t`y z)44aPUzydfc)xFaUHEW`I>hYwvtdHZmiSPU%@AakF)Xp8VSoZ0@p zAqqV2H5(^T(_0WHT=EWezmgCzCotShrxU9^xs@ zBl8XV=M=){N$m^b<5lao&*v#+OI}*nJOC(tTP3;EewcO&Z5dTxv6Hhk-cW4%nwZ8gck^w@fQgc^j`PdHri!vg64@J}%*QCBn@K$?8c->)Au z*$hK3^fFqe?mWoxHs%9kBp>A|%WHzQrb8A#i>MWxJ@QJ(nPY1`f`yC&f5P!uuM(pS zvPIM490>`1Ga9F(7=go)TTI!rm)MeAk7AA$F28&J@Np{vU%L=lMO$_eC1m$?7Z$%* zGHjf``3b+l$Oz<~5H?sclU5U~blgkSZ!q++z6SM$E-J?niMzGaG04ELc(e{8s?9pA|T1S!yYT<04zrgo7=!r-xusIS;qzLM4mCt{ZT} z26K5=(T!dE?|Q~Ton@jf4Ja{xNe1jT>lNN1KUIUlh2F)$*EeMW`dT(nk6L=Nm8An~ zGsO+WFahz)AuBf9J%`7cB+Y|#XS0D3?Fw|?_76c!;9YGo2eo@HE9Iyr%lvXG<(0(I zcnO(+vhAzM`K-^W>2|Z(xbuWt^>L9sp03C`RWR6T7}cRz3v>DZ}99e}Uu`OJml zW@+gT`o|Y`pz#}P!EAh7JC_IYzuGn=SzyP(d$MgAnTVm`HW!aY7+ob(H83aydw`l= zA}{#V@PyH-;i^KV#~`|c2)Y?Cx(@Y#M}Ct{Wc}CNOAzgFg_SB)*6#> zUXdvvKG5P^u#_8`n*P26#sPU7ua9Re>hbbj+~8r^)!Hi$1EY$#zX;Z?xH>y!cJY=) z!M5E>tebRjW>3vw>kaHk3!-Nb)U@AZd-l6rNVd8~M`V9GN;or$JC!5zHh(ag*?q79 zBn&(0+j&Y$rwIC(t71AH_Cv-YYTn()vr$-Ptz(=YMo8$<(D^(00RSz3j4vf00MGQn zn%s{Jui*or-tYcp)tF>)dDty?$StB^V3g4J!NK}!GGep zwZ{JL^Z>c=zQjHN`n%($Nj^jOoQ@EQKCwN-{8Fj0KRzA7c@D*ma7cLI*7DWM`ptn zEUc_}<&x@sjmtAB6^>7rOqX|AzdKolh5Qy&Qy-n3F+HPIEob22E}{-yW_`J&zcNkw z^>9^Z+9dr{+3WAb8GRG0=45xfX{oc)sF6->HQqPejmo7J#|E1tn5b{3iX~$qV}Sl9 zdns3ycDYDG?cSY^z$mvXjgbjS$TO}WjiZ@8u(!@95dZe@IRScO8M^iF~<*_!xUH3V)0qY5OusV>gP1x9QhkLHyUR`0ig*x3%>SH;zi>Pm zOFM1+U2ZekUzC@gNf<=*7`91tbLee<%BJ^%i^g3iS;bY!4f^+XY~yAv>3s55cVHI^ zZgmICCB5#k?Nlr+F&qRNObgu1x<3FWI~=VVCqh`Q-K?a|^ynGLD)h&EhjPY zZnG9!C!f^HfZRYUi;BNuj`}?^DnUKZPh)S zDA!u)8kA6fZG8+{(H0as+lsphWmoSrtoqERD|16OrDGxQLO;4+A&TKw)ZO^%TH4|_ z3i9Q;1Gy!(oY@zrE4s%Drk_YgRMWkks^a({Hx*nfcq0Cd_RhlS+L9Ft=Mb2$Rwa(q zM^X^)2+g)r6J<^J(gV4V9;sr0JvgFZh~$;27Wt5S%VCkZONkl@OjAWUo{mx!=eNmm z68nZ1B{^*oQfOC}_}8Oxr&eTX$L>cXj3i{`CbI6?(Z5BY5kMSuQ zQ=_Uphx4qce?^j|rnL0dZB_M5of)X!Xzxx1*93!A&gj7K<&W^209S#7rq+@2)Q~V_ zK9X}UL9IPFzO4Y0C44jxEiPX4z58eAc#qJb``nd0r|;H>Q?fd-gz0m9&4}T~tw5kU zY6Sg~qh#ZG^+X|W^L`c)PouC$1%;S}r?a_^vZu1AZ)o)5p;~9Lr@~q}f(XKcf_I6d zUAVW+Y8?!7mNt*ihQ$stbq+_x9f7ITzfc}c_qwl|P8zQLf;HTvOsdHB+T`x(A@?`U zMR&@)jm<1yYTW7jtu9&FSq$vYB*Et;`C8ev4k~X0T?!FwN^dLG9TwbV#z{qPMR?%f zJV)kh1*(@D*NfDPP0utPv#vr@7KhIkroJ$ohoR0kozE(*XUH~}c^&|NMKu8lW=((a z7>&FM8VhDI?N)mJ&eED1rN;N?*P(;1XsC{6Y1*pb0!4@wN3edrc}3FBl-|J5>~Z`C z3;(>a16%3!i`8A)_DZZ4rl|9|u#IPv)06#oCocB8D%L27VLyQB8!#TKS0SMPQm2|| zHA{#@La0v7hDHjzO@F8g>inp z>eiM7nN{MRl`w-hbh1ak~sO z?9=WNjEYRWWYdV5tOj;CRU11y9SK6%%Hbrl6TGrERlHAUMpY~Q5|H<1JhyGs7wega zEt59|W#(_rViea}D-oj$gRzPy0bl=J|fvZOFv$Ox`kk-6FdIId7KPjby>Gx2DsnDA*vOTLYz9 zmU6MPO1!P$+?>WUn@PKhAZX{1ygCsNugAcd1J z-j*Jyh?ehk*a}pk7V4XRwL8ao9kyzJ>nrhpMFf>n?Rm< z{2Bn@qis-*%^V&ET`bl|&+Jg8oM$q>ZK#QBEu1KN$KQBhQihQbS`VP5ofNTbpUjGp zv)b&DXCk%_r3;*(@=?SnbK=AvCJAx8XeoB$*~@!r)3-FFf0C8C__PR*CH?g5jY4CR zhBB|^bqtOviBOTPM(Ww@PBfuF>B38axtpOcRz5ciF^M|cXM@u4$gQ=%@qpC{BnXo(%=vZE`2) zr2D?%u1e(ex_8ox18~LzKot9(9{})?y)nG7*wiBGqfO1z(DGj}-Uq9_8f*N>vE>7Rk$sc6a4au^#r ze-9fC|88u84B@--YHK#@>9_2m7LeYjO=EO$S~Ru9JkPQaT&93KtD4S1r>uI2qD`Er zkauP>WLHt|j-5YeB{j6@UPSln6jz7!C}m#QN3zHg3TZ&DNH%@Dk=Q`p26{VOvj%N=4W5` z4k6+K3ExFOh_wp$`vhmjQY%L`TTkGD)$ zh+AEdEH`1v=b#-k!^jr)tJy<7&Q^0UE4^BWcmNIc`U zCVi0$>VZW>tD|q{n*L?RE6(=v;d!2BUH^(O*Ey1~=WDPHyCv8nBH)4~t-e~a^fbzg z*rw35)OXAOxAWK^%B$5hl3m0RH>Rud2sX81*^fIOb4L1l|;O8 zur7GC2lIN`&YNt><1M6mFWQq@UyUm1o*9C8aX5T(6!U7eX=`{sNWfy@esn~OOAI&xRS=JwEbo+iH;7M{wy&!?f#p+r@;3iaa;2(*q z8=8%cQv9pLlC)e*zT;MWZgKOIg5GS2feW4DNrft6~TI$9#~WLHakw z-22}cD_?IpHJ=&$W4*0IX+8R=@|xC8>3%0gJloFf*`1Dfy5!VZ6RlWXIrzBEb$-zJ zZjdvf-exwTx{oDBfx%74@3q6+fwTO|G?ek0s;YC;!|qS@Zz`zwJ+ApYZMhnGe%PJ7Q5)~EN6Qqz+)!$AWLck$B=mdXvzIvWv zc@Xtt)!r-0$))N$?b|*C+&2Ke2lSP_k2ymJ0&%;yQ?oa>9#8`>PVV3uuHP6>rYdT? zIh5H&8xy7UM@gSI(XxDhOj7|EXYzet-Fych6sTzUIH5YKs+EGrC)Ciq*1lG`=Oe|I zV}>tit3*eIQ0i8jy$F{l-d~482H>i-4V;~tH6o+@3)D8V)ah$kK%YFdD&DfB=ri-G z7FHNH3-CkyOOI*oF2F}1tu~cxxJw&UzqU?h*SMP;tEl@3JH+IiQGwm*_TlAk*~*VW=r z(anC~#OCUt7yO1LW8|#wvcbISy6{XPJ0)$1YO-fXhfbg(QLE`hEJ@@5#KqRnw))|d z&R(lPiYj&9_71%LaeY@8pzZLjC#~A)D?!rYm&KesAt{lOLCf`XC5j8`p->EZ_m~5E zJbHoO7zNVt3#*!759`Pm=0O=Z_ASzea=|Gm0i`#2luIJ@lqvKMHp+BM@C8XGg6LRy(_A38G z2nXO<;8cx^!-p-iJw%12%EWYM)6-{fTuc@=DzK9GQa9Fl4?gZ!og#YgrOTk2%yqU< ze&LinkJyo`#*Fw|1he{6b+R(+agVw*Qo{!JGEpT>+FkuEu9$csAG9mT7xovM)igil zD_5o}w6TzKHG0A%c4)Jy^p-wwu0xGLhSVb8coGloiR!OzqTFzfUuj^Diukc5VNR;8 zz>*vPGfsgPsMxtrb_0T5Pk0jf7Ba272U`%MpB@-OXWFsK@06`S8JH&i`84njcTy5N z1bM}^HjPNWhb6QT`|=Xy>!3@+iA>?+)@7APoA9aP&gEPQ1-j5El-$SJ+r;b_C(irTE(PJnpLgANANER2ottC$Gn0}F zW3F4f8B|jOnqEq$01|Eb#3axzPrB}e;xLz&?R7OWn+V&%q<)oXQ#?oV6`8NDE%^?S z9h^d*;IY}%2QLD2m`E$(^pKFv38__=b!*}knMl$XXy+K#rcPTq9G7yR^vEyi&A*wQ zeDrTnI@OeVL5kWNeam%L^1aE(4W~i5vG_U5Y%6uxZ0nwRi{tPOHVna9BZ5K6>H!PE zd~JPkRLS@0TtQ*_-sOl(?&biDA) z|C@&TAHZRnR99e)s`##_4e4`Pyv<)Cv7a#5w!J-@eza(SfjGHGFI$P|`iLr{Z*tZaJOBIl(Ut z;rN=zJKgi5LIo;TUBAfiGd;V*rg(>;q2Yqt_J5^=vmHMuLNJmxHY5PUZx(E zyqz(QUE=*K-hb#hdp6Lr)gqvyr!UO+o@%JV@k{LWCvxmsVWEDUKzXLlSxK)~pX8OyLtxghhr{8Yv6xs&JML=flE=GpG#O z3(SjobS!suB5O~9eCpjJToUPVvOf`Va%`5LNL{L1A=k^b;)04DLWsWiIfAUX#~Q5X zZ-+jC=9y`8A@3i9AI#!NQ_whd|G)B`|A~M2??`p%S+c-C*}?Ri+3p9D?r+i2(5kX{ z=Iy|wVmIg_jZNGe7d<))OH0nm<&rZ9h!<(?l*1t(w%rNKO3g5HQ&jdKHYTNVz*(9d z{x9^Ed)|{ZRmb}$ItO!{?mkZRps6gp{|-AeZ&NOK7;Yu!NC;3zI)Hh1W>`fsZNJ88BT2u!9}m zYu%beb0;*o>5QEqxb>1ss;(>mwA^%LOB$ImP^d_?U==ovZy}Ah6kBs*FSt(%q1^r9 zlaF6GHIflst0oc*B@KoiQqUf(c*iuEmbNP{2^WL=bTdm0H@cWdoG4Ew6~=TJ|7964 zL+|T{@sh;L&AC4dfvDqJNVm%Aam%-{W?cr`Pj|0gj$BG7L(6^X&dC9)w!bkLAKc@! z7cle;Vt2`U%YqnXk{~kYale18yqAcv#SgyS$?Qy*wV&%T3( zGRvK;xk+#DsutK~XULk@#cgo)49~BlgntP5N({VeS7mb+Ow*TAxKhX)y!oN9L>Acg zWHENDav~?(RhOEOoR!eO=U6~&M~KF{)(h{?>u%Z<*XAsy*JSn`cBd6;>eM6&UU!9M zxUTe@Zc0pV`l{EPTRWd~Z=5tl#?loibq~K^iuxpAiErNyi?C)@XKQu$Us~aN6TW>N zKl5WP!x*PqOsJ3#)F*q*Q6BDXn_^(5vsN=36TWBHz4f+_ z2b@V>Wpz`r5AN2LL5W*MKL5r@23(iB8RL~G8dp7yRTNNSwzLEQXrjjg>dQAyw2FJc z2XB_bj%li2nmNr+o;}*;gld{Mf$|#!YKDG3h|@|E-nMX)q{q+~1!8IQT9*xvKbfi5 zS=D;B*&PyI6!SkC*nb)O1pioQt4HJ;-Ve0=#<2g5amxb9_0`ZPyEG)Zf-9wDGv0pN zN%^00h@R#OE2%|DNFf6kF}JSR$7M8zi-O#fYd^~DrZJA&>GY1`G7}ql1-oP?jBAit zeG@QNM>hxBGBRfWYrM{XOs@V);6c~au%I_0=aPymC(WN! z>2C}fJ^1@UCDsp8$Yz{swoz?u2(##9I4Vww-^p_mcD9y-n?~qyefGq0d`Ib;e6#UoGQ9=1;v+Xq0 z8LJQ~I*r{WIMBEvDt((;72hD{SZfyV^?9%JJYyY5ljY|UB) zm5J4}fLi5KMBMF{(k@q^NV$D<3=G{ps({luNA=FdQyq|I#;1o>VjTus?i6W6#7)AJ zV_SW&zAYmUa8JAn(rIY@1TmM<&Jum2L z9?Q0#`@pXSrs}82<)DS!C@nbixG3)6MSbLB@8x-%zUxAA7c@oXgE#*3oj+z0PE(I$ z4^_3=K1Xpagx=&vKE%MOPwGOrCR+gWCFwNrm8<)VBdZ@j+WUEzBo^ngNa}S9Hz7?< z=T^lrKA7{7mx^uKo(%mVbPOfH%;2_jrvT~6ws&#KoYqM;ij3`{nM>~F!9diC z`C-sLT>Rmpk*@KA9YzDNDHEyDKtP-_4BnO{BD-L#Aeu!s?J9U6Fj zrid!g9N}l+h)$a?f$vn6jhQu7wcWFMMGt~6_dRzA_lNcqRopA>skQrhQ&%z*Gjts_ z!=|i>7`qMPQDVd@Zg%DK&yGtv(>;s3q9JaRb$YKHAoz?;sD?Wj=`)Ae;qa)wbtE+h z#lw4g-1#54du|uecG2Tr=4$%hp$z7^TR_cjTsE}@r6!?VK2UdbI*e}IK@yd~DFEf;KF^ya_kgA=6OVY@+ zz%D?c99^yXdGup#_@5ZOfaJ6jNIB5rCYso8m5#+5zF}ox#4|8eA57DSq7NH-;9h1Z{Z8tn=b-X*@IRm|ou5 zZhFI3W=L>wAWm_3?$+GQ^+)Kg){mQ(XqlTk{e$xGFN~hkN9_k||Nqhd2WPEtIjioO zmqsl6cjwrqNZXq@*(6b!OrhTxZ0X9Pg$4MNyl9(OAvj7o%qldsxusj$K;!&hApCT4 zI-QVC!bQA|#zNBHLDAVXUD1QblD_oMOqHU(wR7?LKqgJzJTbIAYvUQAc0cT-Te0BN z?yRSFQNkW<(*7yM6HC%|bS_l>b`_WEx-6wqxggHP@uKgSLH8%^((U_oTYD$xLw6ci z&@KG?wh-UtHdWP!B08Q<_AdP(J)Zq?0K zkE)84st#fe<1E7T^n~|rN8uJMEZa8{ZJKXv98wM=Qg$TRh>6yfbQc>R6W1-qmSItT z?NSJJF$c|@?eR1PxD)ab(L4q#+;0tBU7J)7scYn@)+FocF+oFOgdObN*l4>+(a6$- z0?$c~wjj~EovO$&6NaUmLzBihwPUH$f1fRJcw8{>VRc8L>$OkPTm zZ`;NPtLfu!0bhmzEae40yE{ozJ+0n2I925_Ey)bsnZuK+1G8y4j7hnasa|AjFQ;p4 zLGg+-O!J$~yo6$~hj($3lgDg7%71x=HqI-TL5RZp`CdjGH7_u40QxX4BKOxl-P{7_o4EABs%RzXV*>i)x8?2YZ97EbGc!d=%&i?^Pn-{($5YTkp!+JA>cHT5 zoQk@>mfd-TxAv}t?W?KP1S;z0uzIFwdZGugRu|H(9|4ZraNqLpOvv?VvWHM3 z5;Hk3sHs<+T~8~M3sBIQnn}zT;w@9&FZrGv!`r6YSaC>cAfC`cZF$=e5T;QE2c<+< zjxsn*Y@wh{wN$9|`@APnkQAjDwc(+7_G=}1%-1(7Si}c;UX97f`Cf?iC?R&K5DR3e zN>)J>LzpMF580C&i4qUW7i}n`^ihg6-kuBr7a8QvxQnSVJHTmYT@NrEBix7{`2oH5 zgbvRnUis7xtR4&TlWL$v)~;|M>)dJO%r6@{GUP1)ULhV~k`&n={)ptd~6%tz9dno{o z6{|KwGwzQNy-mNX2MczLfgwA%n4LwE%@yiq0_ymD;2)`HbS@%5)! z-dd!HZ96b#d)J0Rc!9^|mBr@O+P;*loOa?jNVd1Ow^nVX@ZMwH0z03J$VkpGI`aY+ zD@tC?gXNz7=sS9rifHFV_v>ELPE0-5dmG7}8QPo67ld{n^xWz!pN5(O7pAo zb+|z)7ng!r4eMTqKL+JWI1xpf>2)vXbBN^LdwZnb^UIp^-}IC}5*FJdPrRhl|I}`t zH5VAj4TP|Wo4t22`_ZNFLuWW7H`ag1DL7xQpa8SY4RQ7$yQt(&inExautzaLF|kz2 zs7F7_BBF~r!oqe@H6)=s$vQr#1N2#{?V1ZtqHc}yRhv4$IDl?+WZpnW5MdWPk`rf5 zEt4CAK{s|=6{$yoiwr@Uc#r|kfIy!#d>F4^g&gkS(NfBZs4UM+W@F?& zbpB+9Mtpt+GYhuc5owE$r~l}9n2y$=QM@&k8c2K-ID0Q+nt7hGAe?I6zuU+9rE#zA zr|?6y`_qn8B9s4uLGbS+hW~<~_E!xp+zt@Y@g=z{BF}0)r^(bkgQ+71%hnJXff8*g z==;c*U}TrVR>VXl8A>Jf_|KU0e{UHGa>e~EF&i%E+jQ)e3pwBDafmcUdHy*KvS~jy zufc%HRvP2S#LAt)4Q_9%`@! zsNT%Q`QFUa-G+S$J6qLK2B#!Du5OD01;Lk%&eLLMjOJx_zVm9Dl82^f>1z4dcjFJr z(w@Dg`uPF1-4O(CSEFG2HuJ6uhL>WrbO~v5_O$YVeXTehwo7FtGT$ScURp*@{(r8m z{N=_F8h`RYTNLofGq8}U{4zf-ozSJkCtN9=!Q2f4$2)VFP7BLW(Oasvf8dk z!L`IVYyo`od~*Q4eY6nNCBH}Vg>ud{v=9I7x03cTQNcT?Ptn-i9`kAWy=1`nZUn_P zN1q9=8V%qbaQGd+ynEyd()FGbWO@&hvv zO+S#mSdmfF+E!{X{a9oh;V@ez=vi7LiO1?}JM&mDTg|RuqcY3W_Dp*uyJsb%!dcKp z2GoVY#R=^S0nB(~L1;AO4UtWE>|(r4BDO=q4YrVc$ag!REfooul~sS;Z!5kyK{fA} z+-;Z7Q?_-}c?Ox7ISC`6BYr0?)u4-s$ZqGLw`^b$T~5)FR4pk3cD`%7DkhiuO6W)* z%^A?8t!TbY&CknS#7-`r+yTg1jp**RPi}@VX)Cxc9L{a|xa=XicUnm^y_#eqKx653&ZeIAdS* z`Qo3nc23V8j%hgn+2?pR2Bi{-u*rW5>#zu&j$EdskIvda13 zM2Uxw>bFb?*^Wgzb8gCH8baDwxjB^>FtEY#)vlPAsXu?+#rRD07#Sl~69hyJz7SVE z1xLuc0**Z9h!ZT1W^Fr^bp3FK;#2M%0bCwJt9lz=Yim zEzB5qxnjLDlLz=p5yq=?mw5Ub>u$tm@K(^3q`@!m?ID(t^j<1A?ujZeZ~b#CQ5?+0 zwiA&F$+UFU{{0mn*T7TweM|JZ+2&XwQTHY$R%cff#Lfm@Xpuk zH!d!!xjUXy$8pu3?g)|08shmYYB>qERUGojJNylc|C3NE7qizSNj{$X~wV- zS)ElME^>alo}Nh9H`|PrB`OIN-1+IJam4u?cyAK^)_AKR8oD5x>qJgzc)cY#`KYDC ztRYs^Hri*ydb(>mW7&@n5i~WWlzmQf;2G(`w0p)(`rsDL_t(vc>``MTeKVV>%^`B6 z$$uO_867Y8l8S^AC3@*T(HR|ARQ4t5Ozd6o+ErDB^XC>a!Mvfl<(At~*Ual#Deb?t zVN2QG2qS0tn_jLr6BJJqaYfKLIV}5MyRujGObF0u%|sXMinhpk$nby#PhFEFZz#|X zH27}}DKxWi#%4&r=8um{1|Cd|ff(ca7a2zeN(sEs4A#cfe8x2Et(t`s39P5v$u8>B zDjEi-Vw2)w>di3x%@_yaJGul{lKbopI*TO`HUSGJ(x%P^hrCzyQW$FF4f=fjDl*BG z-2~D=q#~m+$BdUF(ZNmlSh+ukc0KDM&L|!;Fhkt=g9@GQXUR z5I+O6xTrjNrf!pdN zTf__ACCT)!gvQG+pI2wLRlu#xMF%ip+H2SD}p+(QFU?z``0=F!08)fMM}8LU6L`2oJ=l9#>y!Ou-&Pc(~JU=S#H?bPY=;RDxDhHvvyU_)p3 z6ZHL6H&@xl8(qKHCbA?4h-xDOJQ2+crF&M^&-B*VIVYZAaapqTrobe__1%&nrcQVr zeF-Oy)2Os1r3pu1l+eMkgT&Ds7QAU}#XoD~Me}0vt;*tUAe$!MetQ%Ze)if{R7-dv zygxJ7&{aMEc;f)&I<1xmZOx!rWBcF zHK$445sOgc$K#z@O+A}Xy`tU0evG763cblpIovw;^TMut>xXnRvKofW(I>Gf4sV~? zDj4GkP6YAs^esJsc=J#jD77CiI6ror(jpG*eP0DW-^)`Z`w*rL4PHv^(xzkMqe8qU zSUh>Y??yy**!CHej9ipc)vwWMR5;DI-WbxeA(NkDGT}0U=yVO0Z*{l}yV4>L0#g{9 zm*H!LA1$WMu%U=@2#wdW33}a{1GSqn+O7QW!yp^BkISbTfxHuU{Hp1-X`EWi70*Pp zFTJZKl}()Wx?3XNAjtbxCnm%MY!;*jVqLvl0azsZqQcD{36QRH7HfC1VfFV~yh{TYp-6YxlNuF*&u^=lfyR zu2V)GJo8dUuGsy9QKj?GIj?Ds7otBsW!5L#C&8PXUG(l|oP#jJ!F| z5U??dAs4E_eMI4U;o5XY`?YjbPpqc{7Sseq*3v$)(<=klN^IoMCTv-s-S6t0q8lfs zdN^WGf1ka0y2_Rev2t5>>ObXz^;iNXHf6-=w$9qSikf-5@}|{bI+=p7RZ7 z+k2zWO}<5^==?sAA#DKczYNJ-jJCXggp~d}qFws^b86_xoN#fC5y;ko4m{W@GA|Ge?xxVV;FSo8nNuGlH(dKtc`pGqa%nKTopp6IOv6Q(SMt~>1xZDo2 zW3!jHNrc^#z$8R|VKHOj+^Zj4p`qJ)tZv0s+Cy~j`o$=WE3Y0+4Qak=ezADZZO(}6 zM%xYAs#<6@R6dhH_tJGWFsZJ+GG)vJFOO`uS?W^Ty0KOr+O^bQi9WK^eMPQ5?rd#7 z*Xm}mT1uTZYx~P)Bi#G3`*WpWlC~8uuUiSlZH^yMBn);OH% zenwD=Rr4MvFKm3T>jt$4r78f9avVC$_0{7t7e6@oloSu|od|1(@C=q6LBku{=4Z^} zU|B_sbsr^0n+t>hxxL1$31!vSw(BUlA7&VOaGWREA|#Sz73{=J-`&e^ahj)o&Duu9a&(!+E^4#m4NfsyL$8pXgBI^Qn zzg9ZBthhEQn3RO~#GB)!;;pkW4b)sZ*ZpvOmQbL}!`dg?WhvY>?Zv%-hbWe@<~9!- z#Tgaaxu>d7gFDA%eK}1JSO+>L37r#d zhYa7c8V_F+J-EWaFnEI9tz{!)p%XT=SBd?pEGGCt8PvziRc9$a<;>jw#;ay%E-&MN z3l=VgVg2=jtb<+ZvCU|Fhp~Hp=6(um_-#YG76tzaXfL|oz&pouh9eEDl`vn+k@L9h z(DRz}AChUQF`quZ$C`Zx1^ivi)rjXPm%fHX_|~DX%mTYPM<@6N;fH*a*L6Xjv3)<$ zl9EX7uE}o{jkIex1LUZA*`QW#1~HSmqHg z=cUw$y1iT3DJC8@qn@Bi?0f7Lh<5v*zw3@uV2<;OOQm4Z3XFU{Tel4PSa32z8|yFp zT-dV;g!lh%DH)C6thLMqPxg9?E|-HnUP>ar zx?Xz2SK-FHV?ysv4SJrjb8>aMd~rTMAq}{w5fS?o-|x%hIJoCP+HARa%0|~RXx`oq zIBFkE8yL7~*UQ*y+t?U!365Ju+56mFtbeESfmH3~j6W))%^4m`)v?&*6U<8lN?-Jt z?l~p*236$-E(%~^Tt1kycx&daoKLwCG0;VwPAdoe$|=d?ql8btM5*VOI+cA_#aTE6 zq}v6384_KfbvqPVvV$yCUU9qBcz|d7tEQ{9KEj7P-q$*zg+w3MF=sE5^_+2R6gO{N z_w`El2|3bp{}*@7(Dqfs`u>y3nwcGrCG#b3uU=WUB^E^bAUC$7TlEP^tLmuJsiff3 z5N5_Jh;SB{IMjhaU3wYCeh^L3Ko?;KSw#5#3W>PbwF*4~x?W*0ipkP0V= z%Jxh`a;Pv@N@z;|UDBwbcX-2oGcRZ>07vzLugR$)|DpGi`~C^Ry1v%bJIW!Qy%NF` zJX6jFdOpd9)`Ppzk1N{}iq9U~2r;?jeQw|g=+oEFRXlWMdr0&Aa4uKmdo)h$Rq2FT zb=;~>csz^CC4yF^5V>e}iayCfazY9HGXaE&iu;Z1kuub$&9d+XgKf43 zBOJT9=zkbU|H0_>=gOS_y(;mi+Vt#V&#a>ETq6kTnSbGXw)Y#O_#1olocV`$NUrSE zdsP70>+6wEKFxr2?hn0x?1)f{30Q*(^K9kj zFBT!h?yx$z?sCd3i71^$(e*>ox|{%VayZ~E7>|qI$JBZl)jvt5Ela#y{agv;W5u8eSUX~CurEivOy4m$ai)d~!f0s5VLUG+SO;c)Z1y~%_UMq_ zjkUY(5TsEr_jJ6NCs93*99;bzC~K8M^^j7Z#yspWil01HE`Psa3CD){u%A7Iu^+LQ ztXlJ3v*+i>%-0X?fh?ysha+sT4EW;gUz2r62YS_I&V8Yo^1x5Cgx6i+^ zzM;%7#`RezG0|n-)pNz2{a_-sSmSLsDWZ1(PIL4befx6qylN$vDIW#I%VFrr=K(%< z={}yNsjJL7y7Ecu1@TU96>;h0#dh;0AYKy9-_y!4z)C*KUEL?*Q>yKQoosXHR%y$Lb^`_Q9afLIAi zo9*(dea)w`1|-?l2#lx0-EzuNf|-qDP^zEQuTRj6ZF1TM_nn!mX?&7ok4U{!&Pr8s zE!0j!lnW=O?#+-(des{|xIBE#B^n|M*K-MCV({xn^cp&Crj_h%@E#;dS{lM3> zZVQJbaS1!iMM$5rq!I@A6vGN3fnJ`Yk=fw?6l`pB;2T$CAw_ys|Q#OqY8JZ z_MIJ$0Z;b3F|dQX{Xiu65h0>WT&3SppG-ZonG8U$VsH_1)m9D}6da67 zL5EGTmwIa2&h2O?Xi&Z7sZ3apN%5d$jp7fMFC9Tap_8Fhk`KzZ_lRjjK-}n6aDUoQ zH9t#H_Wyj@nlQH)NEmgN6GzL*qyk!K$Mn^p8Pj(NN8ZQ(;Jb~ka!pc?_>vGvt-tL_ zO?|W~e3;Y)o4sdG$R^%0FROH}b~i%|8=Jyuvnt{$2cCNv@%F+Gr(#DA4726X4SpTI zXxRTk)p~OYzbBsZ*j$Uic%Ky3uv0%yhcC`*_PhT)efUSY<-gkduOyiFIs=smH1q+` zSy5sj=f<||tl|`X%A6G`V_P`ymAaTE@0(st|4u&BW$(dp8z!+9GIZFDN9e$ez4xO?|@<5(&?eDVHk zDVn|_Cez`T*#zCq`}JR4fq(hlpV=Gt8=>bJ^t5a8mY^fm^SiKDy%Xl zuZ%WdW3?lx08t>b0~7*yP|)JH!cgzO(!}&@!^>1;MQ;cZF!CE?2e7|Ye=#RtKWsi5 zLwPAj6C@8O|LPt2$aQ}UeA4>XlTNCtC0y_H1@}0s#A~Rbn*Sy8k#F_z_m1F3${jlh zc)6pa^W%G`g&=EZjqpHahhV|)w18rzL(+NkJ?f=63uGdY_R{{<%`4gvg@XZ^ps_a^*)iXv=kgafup3e0f73=cBP` zj`mb^J{^J(JfCPLRV)TQnU#xELmBF%B?adh*TJ3*IrJLG2xB4xsP^l= zA4*?Jl4i$ZEAd^Q_H*N|ZLYe@E(vzLmQbSlw+#NTj?4drEb!Nl`~`HfCw+SVh4u;T z%<$5;*pvHKavJB8!5zgt-A-mWdg-Tgsb3TwGQTl`uM4t&V|3m9;VyGH?)&S$XuAX5 zSNq}rn*;Z{kM9%y99tIXf7N~$vs4q=Hr4QAXq)qGK>g*0-7=(Yd2Odq(J8k%$~Lvd zs#^f5Z(3Zfo=Ol2%W<}w;cni{K%Lf-r@C>SC&`)Tj`Y)@7hx^hOEQH>f1Bvj-lXs& z@V8ycR&8|E6;Y;BQG*p-y8$Ar3{b>)>(R_N#7?E{O;Z8#qs?PKsdeGfG?;bjBj=_= z`4Sw?&t9w^R}Jol)iUu`O+cj%xVW4`X#5>S=VlUGzU(es;Wzlh*Vxzjo`h7Gd<^{o zX~cVD*$)y7geyoZ%=bBvnk7b}KG4@Hi~npr>6vzpD1$_zd0TeF|*5xQMF7 z(4_&5&C0G``-t^;lGuV%wGr#zzH%%u>GiANw|JO6$3`9`}hlsH>Cx| zi`!VoAnq;x5n5*tm=dq-54UtLq1D5YyE;2#d03^gOe})f&yjO21S`x$%q>xYfd&s8 z=*7!1z9yMGOMB-@ZWf$oa<;599Q<)!bXJlj)SJyD{@Cyz`Z@l)!1FI3dw<#Sufme_ zKRsUlH(#RX+JS;*0jp}A)*R1#r*EIlWC3V2&7EvR<7c}k&Uew?;D=fReW@oBVj@Xy zmwthAw-i%o#k{TKR`Lr}a6~*+(h&TJpAqlW*lvlSzmVyEw za7keLga}`AmBk^K69XA08p`oqcpHWb_u#7QHCiMg#B`sz7&PK7EcBCBTXc8idYsQI z3WWLUedsR#v9C1kRJXWNQV@!7U31VH^6}YDtL(~jFF`b^U;&Bu!bV$Q&fFW%ppH0I6Pw_F)Yx);OkWVt!jK)imYl6D?!#UgeZX!c>2e*#V~c?o=`F_E)$q1N8vC>tqEeH*&2bNaEFSTECHKaYk4ozrz}lSkFStAg71H zAdl)MO)YP3aI{*N-y>l+DviTB!ZN}Yz4hwnp(ZG$ z+jYS1PQzz#%h;SY-(~7Emb@5BP0Cj~ zMM5u3ESQCp9t$6Hx^eoKJu~Y&CnwjP8~UQdN@42hDymX}R2X?VJ21rIum zqB(_fH%m&%9r{EL|F+}FSxG>3z(sACb)W~^dr0!-nM5{HR~RgK2t1Hv ziO!UJ!!efJ*Q+>&7rB5DY!SF7?apF`T#jr=?XAD+h=3#FI!iDsR3WI)*N>`FalSjb z>gV5dHK**B9@R|N9mj4}>5w zd+_;d&=`~X_jMqwETtJZY>joH9W>}vSHrU^qq!Ym(64~;VDSX9! zoY0wHQ9U#BP7h+KFTobwU@&)@f`-ez1eCdQQ3oo8yDhS_sh^wht{yhfO+62f)vY0$ zQ%%H6$n~<0pQ3HKW3>oCpe%iEZlAJ&`SVZQPBWx*#gJxwio1eU!xi+YBFw&QKrNAP z>0pAO`7N@A%7eg}KfKdNqjxICW^a3emo}OUnSHRFs9K*iT73p4srJipz%txO^^5Zo zK>}sE4Zh<<>rgliCo~~{$*n5woLcWSC*kUWo*$Ee7<;5oMOW9<1t0IaRXxWDITiQ$ zcG|;;aXE}TYrBuod3`+ELoPI6&=L|~f`ljaPV}4Y7{IB7EQf*oPvCT3o<9{-$qsc9*k~lVPqEdRek3??o(zcV6ovn?{!Bsi|>kU1kGUj0*45kpw!e zP8DgFLoSkf8d18Z$=x;3Bqo-TOKkD{Csa+D}Iy@K563`+KK=6W}H4{IKcZxcHt*7a0-&~5Kmz}BBk zHNWb)CDNKOsopdj9E{|t+vmluW)*AkW$5p2@f=&3wmrL$nd|_Pw0 zJf@M0D{4Pkzke{(ueVxj&NXxiLQjQ@0gp)?IG68cY}!WP-4-#0=fwE+LIw6IxF#u+ z262BBSzZ2mm3Eh=(OzNN^IZy7 zw^DiDeqcAIg$QDniBqU5+ zmpR;|O-ei1_7N|PKf-lTBAb0G$eNkCJ9YU-P0Pq50UXEII;Tykz-HY7DNQ_YA^;A<2lrOodvFss16bMAquyi17Jk@AAzK_)3WWP0&U;G zE*tZ`-+9#1K{WLr933q=9Dt5D1`V_N^ziCVLEoD z@_PDfKt^;*Dc9Kr$7^B#ML+pjn)BM{$4hwZo=JG`7pJR*7rz#5&%x)X91{hS1jL6} z$}JI{cHLg6s*mj>{|{$x8P-5Ul}^qhcBv};&DuU6LETY`2HKWntsn@mkNXo#O-ZbWqdOG0^UH!N$#hs zLIG+y0~}Nfr}DkomurbS>{HDx`89R>K6~N}7XnW8&&a*iS8vR7m1L3cM~vt*7uslsQMSg_{z-yVDw zd>Hv|kbc15M^w(1enYlJ1cPs32+g!)=OU9rl=yN&IGm|RDU>FMYqM~&C1NE?Ogh~+ z_W4l4rgZm!0UlV@sLD#S#H6doUc~+BW&_5d%der|Q|D^7PFDCfs<)Y@&zVL!D}S5T zPPgId&X9upX?(B_UFjlb#MHgYwkXp_H7v%qSY@V)RjA0yruaXwhkuUR%-rNr#?gi; z20AS0OEK$sW`4*Wm%z`RU#|X1(!S*|H637M#^KAqI)C8+PqPA=}sYA}nelmSd_ z%;PN~{=yf*2gBzDSXx2F9R`K--f&4(w6I2k+97#8SN^hlm86F}v0Boc*+0L;gd z>e)kQY^hJZlG`NT2;C-#j7~W`uVD9O3p+#)(VO^SBfF-r-)GsE3%uAq5P9ZYm?efv>OS|vuc<=I5p3Ri?(}bH{WGcLwtVAL# za2|@BY2QO)r{%Zc%z(F{dj>UWeq%OHMh{ZRC0T?LYUXU_%^MoeZ-!(E{d)z#u}JNN zaP?(Pw3S|pl}3bs-Qt}gFwowH_yMz*Xo>iuM|6J!Xh!h-&GfhJl2A;vjKh?iL6L7c zpvWFQQC)aR__cLSC$3X5#!AixY6p+l{vA~=wEbqHF7iR{jXVbcKs$7z z$T}zOcXBElW4^>0o!?_DlHa;Z8)fEx$SMYv&R=M#2#e(c#tJFBNne#>O>Cq(sc zE{s?F!FW7>6{F&^myW?#W7R^8JCZk70>tkO*-5YH)OV3(vKWfun_j``+WvGo238EN ziM>`zXl$}u(;t<-vH_KnFUKx{7wmGbrw>bB(yea54$O0qQ_Ud(OdqWlY+?oqgmE5p z?mE|rc=%ulB;4WMzf)+fiejV`48VMY+YNzhj=RAkIf&>i2!KbJy2_`yCSNl)XNYpQ zHBtPx9t7_3g(g3m+{btL-{^w|2+@nm>Gxs;(}Sm8)JO?YQ)uGt9-51T{ej3(kxg0e{cB^Sz!m4}0Yi9v4 zoH#$6j92JV|AF<)K<(N}o>z1|OnMC=a!Jx|zVD2&G)PYz`}#g zG%!Q0leG8S(t{{C`T2R(%`@Rdp?oom1y<$gXffuFQRZ3JA>rLc-{qrAIuw+gK1CKs z%jR)9^Y-FN_hgqnxT%0p*tZUt=$-~*E4gFy_uNs!wckLZXrb?dX2T>}@V zkuEF|5iWEU#&fqtl8^deySHiJDw?okSP&fUuFj#BBm>c}@50YAEi=bf+XJ)sdUcKm zQIiI(rq290imFlQ==n-B4T=sxYc}FwxneIfX4EOlvc5qpFWFPV%|@>(x7Pk$^{2>y zN*^hzFRe#`4wkFU1k_@Omsw}FzsJ@q@Nbg3&O~QGI zMN*Cn5@UR%M4^hXJI0u=0D_r}xutU_n2R_UytspmSsenyBbHkae%x9G7KHRbm|m;* zhtv^g6o$uU+SH%z_;^lM?e0CG!y;QzOMj&XxK$0zx5~&xP&^Njg-0kkw9rFqcf6kL z$88Fq(ZyF7(T?;qQ5^$`(8A=z$ zOebQ87BK?Xd5Foq{9bf=r4Tojd}Pjfv4N0|UyK_;__A4uBnFEmiTcpGUVg;-r$G0A zV{7GCPrYcje#zXy#5oJ+IDG5I>XHtVMjuzvGekF4TnM~1azkG@TOOkyISj?#<1+6u zK4%7AefhKnN-Yccp|)N%(kwfSps!GPbLL=H$GP*6Mu_(u*otLkbIL3=w>hD0rbr)R zwSnios3qjM4-0zGZv%wY3Nrv*CK|G`-T!(x>AxB-(aQMTZTwv?Lr3#+J&V%jy`S;s zPaI+#t}KzK50>=4AvO&uAI}V(IlTB(tv^F7?fGh!)*vbB5P`?5SZiZ6S9{|!_3H25 zeLi8h5g%h2p!jMpr2TA%f1bZEu{Tw=s(^;ET5|cS?3+j}x!=-|ieVLJI*Sdn+IaWQ zXy;JT%M!-aeFv(o^k_-~Sejmzvd<2VC*AkIC+NTWSdXLNXhM}1pk0-=CVWqKHCC@H z7iyEDF>PH8h=T6UUQYV_yi^F!c30S`(O%XnQqrtN?)o4oFc3ESqMwQH{+hdg_3qU# zcJ2vtw)a3u-?PFl`Y_V36^G1=I5A!d>dKjZ#ZXesRXa4L)xgxeMbA3r?V2C3yNG_@ z8QeNNyyKU-A%EfhUFD5R`|XC}EYCbkellOOH3=PE7^WFPKTydgP+`k*v2im19}`ONct5oi?7xuHWE3VXNl2GqfpQPDPp9BW=sYqFzXIaSa?OljBNv^W@+kZ~bRP`9)G9Cd+d|tNIv~ zo!v@Uei0tVh4AC*olVYbA3?~KySQ;{G9`aJ_`3lCfwE&hYiim+y(+w^1_ zsy6+-WlP-P;Z0t(yydS%-p9+Xgtr=y8~nnManQu1^1(`MiQhSb%0Aq^Vy1*aRm=Pb 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 literal 0 HcmV?d00001 diff --git a/ui/storybook/public/project-skills-evidence/routine-execution.jpg b/ui/storybook/public/project-skills-evidence/routine-execution.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a600f34bdb3a33cdc51745cbb304368865fc935 GIT binary patch literal 80344 zcmeFZ1yo$kwl3NP4<3R`V+kR+JA~j6AR!6T5CSybxI^P4c+du z(9xIx4@l6^Nzm@v0Ce{k`2Zd5{s8>Tfqs9v*bmTfaPNzChyiFA4*(bsu&^<)Fdkq7 zNdKsgfk}e(h?Ma$FO$q0&9^bwd`8C6Ux&$l``fQ|XL3iqE$ z9$_%bU_R!3qiN)D-ybQTf6Kzb-4p;H{k}E{Itf4;@LMUwFC+x#ADBQi5@HgP|8E=f zET#wjZD4Ct&ae}|xvDkbQ@Ev$%F(eY#hcoflsnb=8|zV68A{udp4af>K|0S0xmvHj z@s^-T_7nKEr#V2N0U`QY9Co|Xkm8bka(#71kMNZ=UOgutd#DL2uuNozeceS%i(EPL zn?%iZR#$~!%eeL7O%mdcBgAQ_%+#X0pPb_ldU*@Q46jo-*<^zV4zZgqDpo7_KmOPp z>iwP02w}26{sFduD4!mRe>fK0xm@ynoZgWkagm`eg4pqHyRrhNL{n^>J3L+t{G_fy z@pIVzQ_aJ}^$zYJqxXvT6pUO|xu%aD%o2G#oV%g$zwaGbu9?^gk zYVH8yP#6`+89g{lVN86z=`*rkH?>Zi(aNGta8`iKC`M$pYX1cZa>SG2Y~I)Sp(fWx z%QYHC?XJMWrO1^+>j8nFZc=fFDn_I|4IvE%;ccqdjjA36p7DW}`yGHHtlE2^ZDD4b zZ}2fF!>qQx#lP=K1ZrDm@@TFiFf!`~x4p(%UwV2VK`2&?^5LWQUUWsZ-3k8GX{1yc znQEj@qyr__BUcS7F5`1+2g%xJsgEo2xF&dxH6%c+dakEX&(pv-ru8QI8B+M7XY62X zpeVC^f;LeWDm#ov(M?L1FqDSgZ|}TlqY=(igLUv^_jyxPEJb)Xz6ob1GQe_9ufNaZ zN&qX$_c1M!Lb=o47tdXaaZs;!+19>8s_lKDSQ?W!IgnxAfnn4`G1K-oS7p%(^*y_UQ>@+6C`_2zAyaM-x-;M9CZi}n z>`qF~0nT(C7tpg+ng1+N>VyO4bQ1`a9i|89K-AIQa6Heich>+>vMu#N@Ig zWMr+;zyhZ+FbfxO6FgomdP%l`P`FVLtJemtJuG|lfj%-6pmnc%9u+Up%J<)h_o7352k)8jHaug3_& z!)WB?EO?b0fq(4Waq7Tw1XS#@lYQDLE6jBmRyxeNBxRl13S2Mp?8pml1N->Rg5M5@Qh%J zP%~_+jnaU+MXclb@`bk9mORaJd|C$GA@PJ5!>khlHpzhUu2gX%JQtJ%qE#mI!W0)| z+JFB9YMNYv(O9p6VVSKsa$JAvst|1Q_?PxfA}*NS{*@@GsAo2M44iycD8>enXAfBo zQXo>7`Vif>G!xlBjA`SOD573XG+7y2OZwYKK_SA_sviP|Pk^{PZ%kuS*j1qDM!q1jOn zlmL$L<-WtaEGE2rDf%#Kq2~rER(mN_+b=fvlhLdC1^eA>2n0^;=c8qMhMl>{7Q6_3 zI6i-mGPA+i<`Y4VS--R?W19V8jOPoTuV$x~8WAVkO~jXtZQEbw*UO;(@G5Pzc5KNy zy7dDhdhLZ1MM4Ekv<)3$VP`ZD?QG48{o6fgfN6lQL#2wZKe=GiG?@3W?ewK7BxSV% zTL7-*PtDxe0+M2AUFgIc>i-S&Mk-r@-nKd;2Z$h)`=<$h0^hn zAC2X4Y4ruqR4(W_3bzti2=|~(U-h}}Zd+Q?RhDV}$iqgDXdSL?=UYy@{&XZyYC(>M z$7~PIapRG=O*kIzoow_T(j3;`SP*dxqW*l#h>VL*X3I4NEAgg3%eJ?SUuZshq*Rthb@SqP zjW(?^r$NapXN~Q%&QS~h9m_Zh^gc=M!mU%7TV|*`guwrI;TS8z)|R0lJrTIL6x|>k z(&Tv*eWBm3jelp3%6(ub@|57H|!0KFYKC$hzfmbQp~wVB0LfjfNk#N zad8UTWmX2#tJ@IKFcv+y?Mqgcdtl9NW7-%IR2J>iN(8Ri*(& zJW$7*i*2v0x?Fdo5@W3zA%)5>*gWX{`HyzCx@_#u3)iRefE3A)kB;K!3D{C?M=uSl zWT!f-goxBWJyMz!ClDuiCThi*NH`3Ck`P(Zm%Po}bE~98WF6KVHo=>5n8A%UwwH$Pr3Ioq?a@W-NVCM zn1J5)P`H%2>+BFDJt>Ngn_OWdMvS{DL_xmpk-i0z`4sM!-ub>zi_L;hndu3_1x`VW z;NqO;W(VOBWhS4H<-CxS!;t&$GMFa_ZhMG~Q|J)DI;N3x4byi=l5`8R%{w^iON%Y= z=~CWOG3hq3__0nELrO(j0#gj@mRE_T<;3ChgFuQxgJv}@^_g4In6BSXxL{ud{ATy+ zf|KbHpR)$lI3s~As>X`FY@`8aH8oMd=~|gDf82^pMo7rRt>cf$h=^|8fTd{~PAtcU z&KIKjp7VuJ=?OVEG|gzxbn>*87jG)dKAGrN$^bV%QFy`+3NTh3jQCf!> zFJ8^~;wJ}%)v5;9^q|gsYx=di@cN0~JAm?J_#L3)qgkH7R2Nu*Z#J5yEtUtODLy*! zwyGN8ouLvI!gax&Q`GTd)pHEFb5p27&F`pi(9_jeU^_oKH@ofa@7?XqVp43l2R7<3 z`bKlmN!BZ{MVV7N?(smhR&ly#=-Yb0e5*dx7d^oCNzJwC=CB^cR%%r3DsD4+@(+9L z(nE&NslXA}cgs$1=Q@qK$9mPLg`Q3M5BjOhwe-`@G~WRXHiJUA$r@wIw!6&C>oh2$ z3KN`wX0Vj2K{!bfWuf33eTYACfnO&FE{$ z)3f(+GA6-cv*Ze&n@}~vDREvz@?*lZt?~VvU9*>(o_edNeuWkxcv5+weozDD1rgV! zrf8j)_~lISSkrB`;*aLP+>-JhVwHdC?#E&p7llQ%m?|R*N`PtP*=;W$A>9Dt|(&tA0!AE^|5;7Xwp@7wuO;w|s`9fh0f}^l)=$sdY{xcPH3#wJrdeEnhENZ#k&}8e z1F_4^%keBZ3j|a2H#POv&6$8cqdcV?YZ&1n3*DLCRK1=pM~CrFSRA4?5jyXf*6*gE zZYh>t@q2Y+wuKONdhvbndAFy8F2M{o3C%zdo=|~IME1)fE|OMKR+F3nBIIGq8mdFzg$V-}F*vdl9M8 zrHXFV9pkLd-zemVZM^$pM}W|NXopA;v53E1+p4bmzL2lq^I+U1BrtFdLOIS;PeD>T z?qB8KWYF^~3s2U)L#K3fq%fpytlaoZw&c1o)HH#l7ge2_U*041yLo9#e3B!ddf74B zjIoYD8L~W2I}n1LDOG8vW(gme{?H?;-i<-O)cf99#tEoYYn0@p+B&pV$4zfi9ev$k z_5j_d>Ozuv8hZ6!0q@a7C0!CdI7K*=^7Nvzd7I?sol?L=^iomoE{`vX6!n{d^GmEk zCMnuB-LCZuT-Nx0R`0WF($aUnQ)jJ~WlKD$2xpr@vwW3$M|!hCnrvv=S;L?MT4$my zgJ*EY{iw9Gda3Y4W7QFac1gxo_3=%^Zj^H9amn*OTYjKcY;KHL6rU_jPIMX^8lGXW z)cZw}lYoFoay>njlCb&>cJA1>RG&xLHNhlnVSNJ}DcM<6!vQT>U{&Uxymjs-lE-`** z)slf;+ex@OF=MkUU^skiq)k6WNINWyD0a z)#yScdMkrdpJ}8ybBcho>%h*okT_-zEjY#-+E<0*)G#~SVKjcdnce*6Rs33iF%PlC1+gLVQRW7Kqa|*#twXkNaB6x zPWPvA=Lt{3M^uu8H$j6X-U*?^Abx#QDFeh;bFwhom%lA4qfOMe276>Cr^BfiUvN25 zk9hl-`|^8zd}>@-LKe~%0V^FgXF(a5#|01xyO9gF!Ik?U%6s#*vqr=wOS{SnEh!8m zOSI7&tyLL6TQ5X%p)oR={jOMtIxDNJP}+R3DiyIEcoD*x`2BLsBuRx8whO7Io%uqA zm3{($#tNmO85wb^o0ASZ3=twP`v6iXB7=;59<+|Z3Garc;9w+*TsYCUPF1e|Lt5%T zz+C;GtCa6+Pe=4>Rh{=mz&ew^tVhXUf3$P)3*i)r2qs!8ku}QZSs-yBc*-ZFmp#3I zO+6Ne$ly}8b0GYAT39d z^?&%@04ErBuuoFKS+QV_i@ajFgeTen`B#b;9&k+Ubc!C}J<@~a(0?_tabue|6o&{31d)(%z1J)Pg=cCMH}enE$>4|)_0dqbf`59=b+Sl!YeN66)A%kcL4eI zhtxjv7R1eZ>%w(6hG*MSb9aC)hBkR*_hkTb6k@M%@rAgsHXfO0NO4Y`Goo03!eV;h*F2&*||0Yi~Gz zXw^XV0rs(`2dpz4Y$Y8iUob8f1A6gz0){&d!mvoNp8O%O`^oM*HzR{Y+7GP(c7Bg zS5^j0xn#CPJzFe+LkX2_*1YNz={LD8o(byrL1(3xWmHg!wE02OsK;B|j|np$7oUSC z9nRh8$rRorMP15EClfA3t!mZ6rx%`ORZmZ8cGrlp6N?ZTkk4f`p#Dr|I*TQHND7rB z747eZs;Ke$7@U(WC4rFe=aqOumU`9r`eU0|$(eoF3{JIrsPv56Hp*}R4}u8KUkV6C z9&+yz=F}85Mf5CZy>~zJFzzpVzm{in|Mcy7`=@=a5T+i{gnI{I``eogHJcdUHS2Zm zU-o_p{pZR59I$^*61M-OV{mJLlTW5r?*P2HD}O4KSe{G_>>mNXm)&=OTI)N&#rJPj zloFL`f}J|wm>j(9f|OPap)BB_aQ)F$=?lNWuZmn0pU=(8KS)BD#w^w{D)_Pc9km6- z^BL{V>`PBM=Vo3OIBZ!rF~8iIR!0h6^6D|i2_muW-c%4h^%HnJKP`O+Fgu6b0h)d9 z0H!$urbTA9;XR0=0JjGvcYrpl^>^lhyxL)$ z4gI!nXTbJQeC=y2IRY?~HtWo}F^HeeJr*Hf&W%oW4t3a-IZ8 zY=44;H$a%VgnCUklct@C&nhGy2$)((re-+{t_n4b1=GD{&G02hA+5r&gp z$yQ58Nb9(+?v+W+^@eTlL3cxhy&&WYT&(pottnTgS8MQ1(NCV38O7=!AloA&_qx>m z_VRBG{-0bXCTCy)ZObG=3{5Tvc~6jWUw7WFGs|zlR$b#7gszBRg!!a;hmP7IF>vxX zs!{by&2tMF7U5123)Z*VbX>AjpSz~B$uHId%I8{2q<%xHOHhHY!kMguzUCS%SFV`q z5kifU`x(o~AxQ$4Cw;=#KYIhw5|Ct6>`U7{y;<5?97b;Mn~o2pITxe$>o0t z6;zcYCKAXeLo{%sb6l8N{8q0NrW9^}B8UFkAD~{Ed?L$Ub`TZ}WP1)az}eZ`d*ahy z+^l@UwoAuia*W{eV<2zwEJs_$!Xv)EeEb9IHCfb-gct z)BEAWsw?o>EtTUq(`3kRsrQrTbtmd+zzg?VbDx6yvf-;c05+#FXXgD0b<)}CmqpO` zDdR4TEEV2{UekAgVX!a#7;MjEEF`2#^LN?)Vscy}g?G}#R>E{)z1+@)<>ka`EvoX5 z?Y4|H*Q~*6oHan_ciDs(F6{^n10-D8@?L_eA@*eVA! z!jZ^I0aY=TV1^pTM``qrME5b4Ii`!pUC;p~J-90@QBAhbE55`)`7*Q=X*|-FPBDm? z=1ko+M8i5$Ck_r3UrQ*M5<;eEz5G{LKi&Zbt%koaP0P0GP9mAv2q{>f@&w3Ny^s3& zf`xRj*r1JMZIfN|53rZ#qK98Vm*2u})pe%O9gYJ|8^TMWTUH{p?JNXOeSIPKR=?0V zK5?QJqjH;P<3QzSr|`rK{Q}0bGKZS0WL4y7$2+9sOzLt#y>0aF5X%MvBORb|h_|Z> zbfa)p@eoU3n}+M}>%fK^>Hx3`KZTb2=@Irob(%^lFf!x~8~gp@A%7p1-2r~U{^x?n z#9ai*_&xq$jr0p(@(FgE-n18U8yVY=u{5=s1+Yhks(2|>e_jY);a8#b_p?3z3UeMO zNH0FQ10>x6W)=RI?@7s?zz^R(GrH`&V2iRV*x%ENATM72Hf$4=DAzr+>n89>v;sS! z)AXtKXU>t1YDR;0NJXzl_*PBKaG7Wb1D!PlvPX6Y2-@Gf1Jn?m)!K-#hQK4+mdtbU zb4vs#MV$ga*}3*AVXM;(raXLkAUp=_H#LBj&LK&KySizIn+(Nt%DvxQ55278#^W1M zuf|*~JHK8PTVs7n!yghSSr3nI6I1H6lW#jBUaga@IX%d&>OA0bqk+c+N%O}PDySVz z1dLA7RIgP97O#sNP++QeVj3qqoRrmGl9#Q~MaGemE4cpPGACXBJv8#Wwb=2+;ndhZ znGTgOV8+IRsoJX1e&Bo&g4b&lN3c%BxlW{A;ILknjH9PkI#)?!*=1Tp!|&(69H2jN z8~OyXD*z=jPCY#ZBOcFJ+MO8lCCx7a`&zj!Khd48O|G79?^$r%lj%(hGkAA^{bT8C zkFlL=Q91i!C|OKyh|RVwZgrr=(i{*!1-1j-5!o5AcXNeR*G`P-tBmyuO{3fBq?k=a z$l^q={1kei1aVuQjd2!tV+XT7Hec>6`;=Ut`mKx~50a`$*=AxCbm1#vSDBfy@ho`iUq@zVvobl->Z z1}J%>wl&xkgMVvibO$hrK->W)_2vT)h}JLmHth_&ucKUF71IP+80WT~i>#Gg5mYlJ zG%_i3P`zTjWtha?#E!wa{9+9OTUnvP-kOPMU~}VvgMe(UrJ&lV{U7lq`)xmro?U*< zdElLszGvj1b%14B;}mME$G~eokW_jL&$!JyS)1>+6$y@XoCLX8YE?Q$N=bNo@{NU! z{&;(wjlbrk;E$r018xR>@iz}Sw_DGrAmk=sHzC(w67*3J&lRYH%nYi^->YhLGfU-V zTJto4Eh~rmnBK?8am=E&U#mRE*RWXcjSRYg=SUW&RPE%A?Aw%!d^06I#t4r6h81|>Ra>DnpApv6yH-w> z&^*(jb#vM*NL+4oct}*9l+Li>tL#!59LyK+kXP0-QodB?iG)PlB&mjoGhclS{SH7z zDlrRsX5a(oJ-12&%=9M}Xn1-R6tISMi0xY&3ng?=6MfOw-&5_|k_EA+uG9qbCk5c} zO*sDmb7{+{%Nv;vP7kA-_yS9QE!YAVX+NFt z9>bp3;4dxLu==(0bV}wa;<{~+QiX2Q&UT9ETtA8U>F|ycqID3@-Ws*o%dsKX(Ro!u zM6*vCEw|B=-PSp_ku#I5R7H0)$g9XAr^^nvH#9$|WzUPTnrndy~I4fjr% zynIPHDS$0PGvw+=bF@wDdi{!XQ60bJqx7eOK!kbN#=F#?6M~>sM%~T`B*6O$4el_fNN_vfTVxJRq|LH*yBKN$4LcePwJYD zNQjvx|H$y;7ycle6!*0;XiNd5rG?8+vN|j4O*)&hv%I$%Y)77QiBlW?e_-Hrj zqy|_Z(2d=v9Y=5k!5Q2rax5SUQV$9stUHE>FKoIGJnv|M!(xo*I!3(p<)Ryj=CA?< zVbRP6vuAF}3@{Ha`YJb(Kz=9{q1dh-BN#~d!zH)$tfSwR+ER=f)b?MdLF+yA-1n}( z4EFV#xC025ynUaUwS?^J^fs(Etk;|?BrVY0e}q#w=TI*Arl(~r#HMuwXrYI@K9-z(XR>{I`utl0wi_uH9Gv2I@gQAE`UwBx!zDK;L!Jq#`|Ez6 zyVf%vLMp141M#CP;EyJ{f$Uuz79ranS8ed)0+!C#uu2(lIE%9lsbx#?nw?X_`uf?< z_VD|R9NTeeN&8!#dW-jld$@;hYOf#MjU~ z1f@4Ie)?Q5N|hOWF!URa*KTtmGyyT8@C1~fH5%>uGAz&M&?;j?HP?7*sL#oTs+c?+ zuH)3icRFNg)uRXCX{jHKH){Q(#H2LjF8wU^A>0X-2{GlnqB#=?GA+mIk9DxGi23QS ztEECNC7Oi!$H8QHl(x_J&(mBe7S1Cyi!P&ZW{ljCRrPSh+|%eLuHP1}x)Rc=bRPrR zmRZ_|%8jMy>rAMALt1lfPAf~?D*RmL$EJJBmMsEKI^ZRWVIoj=&G_dX{nTzN{!zkT ze=(0p@`-vV+xEH?ueh)-NxhkKp-J8T-SE01K<|YIiwP-f@!P%k^Q5k(ktGRE$@-u7oAPa}JZDk9Z6MDsC zPs%y*93}^6qmiH**dsB!;rBKL>(d#ZQJkakLzABum6q*N^~D1MEB2QkKa(Z%7Y zjE3h+0w79gJQAWy+NN4zLUF3tO3(lWHoVJ-+YC|7 zY3wcwd+x1p=&4vwK$0jT0WpD$<+hPph#R2e`gQN`h`-8LJ2EwNBhs3&!O+Lj=JQ3i zJ#5aDo&#D1vPm(MeB_7Fd~Thx;W*KMr;dCRBRE`!OMR3jp7=w`SOYPAG)!wl?3POT z%7lD6%tpsFpx9Sp>hY>{U4oem(MwkAGt8E3y*-rYIDtCQB_bV<-2T|gd@?MhecRgo ztFc;WD>IbsNU%Y3!n$(G!h&mLF?@I6th~vJa3gKuoNay)R|FhfV8Tz`Q9qoLP0ttr zYClp!`E%MXuV4hoqSq0N7keM~0WLKLgMxxWQvi@!KU&zKRMTpK;Z`@X%S?w^g#eXV_CjB|eB9&!uM@t?kJn2WgGC2`knz zP49%0PJ!k~YnsmF_1Uo!NvkuW@mfr!?0bEGjlquh3VL>eP=K=ZRmn`ys8h!%4VhX? zCS6GP?MwGINfLH3{j4eHm0!$41=PVg(!qd1S*T|e6kSwVeQpa^l)u~?4$v~8{79@WXg5&w0m`?mx!t?;UN3E`m0+-9+Nwd7b)^f_POG=#POi0|PL z{C;d|a!Ny!vg-z9-W)zMC>6nV@c6D>Y{F|(VxZN{NRCqXpEs1~$;-ms?U~%wU`Hc} zQItSm%W_4UU1ndDK-%Wt`3evta?TT4RM^(KMkAGtw)h@A;bv|OY87>L*#TA7_pFN5 zTne&>4Z`B}Xd_+BC@N>#qUiPdw-2B3>=@^jkVJ`=3A|ze2)4A(e4T&i=M5pB)qA|M zp%jF2a0( zqk9ZU>KJKESk1^wLp621*A3!csG0r-0oTOVg8$orBc{?hP=(@117s?{_AkecnnQ zo$P*dAx~)D z09VJH{Bo=GkfZ+*d7A%bZfu3vb5ne(@sURI$NOeMWeU%D;!^1%Ot@mAoW8m+Y#1op zqhj-%eddf14BxYms1|Kge4B5u0%?neqJXD{*e*GK z5oxD*b12MXG#T}X77Ly@74SW*U6Vtgg72`?U@5{y=M?ve?V?9KLbBDHo5$_zG}u7i zaLMYe57$j^x}%|&HJHiXj@R8cnc>0_eg~lcEog!8pvA;@c61M|T`5|{rJgR0mFgnI zx0p^+xG_A%?lxcH6|h|?BY$$!uT$I@=Mer{ri&6HW(AWn zqtlduz5zABr?U9>USf5kGqs$S;-bUY7OGkt(XE?Rw+N;kuMb5JC1fE%_O2D*4&0RG zw+xe%1My6SS-Q`)be)=MbARH|GgNJ#yahOn6EjZyjW!6{DQ`F7!j+km&KaXlr%C6=9cPNKxg1$TYFg zmRiT#^}Z!4=qvw&!_(CvYR09p>86Z0o3Wye5!3o z@9DL$8GDzN7(Mcj{f`~qSr}$ewP;I>sN&WJOq270IUWOx&eYAhiT?@n<8h=keU^?dz3Cv z;q~Gqo;P4&6LShv4O9C~xdL@%s`(Q$z+!h2Jt}RfDnnDin*gX1~%OjwQy&;=ds1uzanouGSgzo7mVAVL3*D!a>k7OUK315p?jUpzV?a9 z_}E5*-D1pIR$=3`tdYI$^Pgk|qpB*;RVgBt=;_16&aB5$u8g;I zt>3<{1=e>8m(JI?I(yxyAfuP`+zLO!P}j~*CKx_ktzZwYJYm6p7SZf zctE5YV+^QB6Oo7F61vI`5Drr1mjTpJq!W;?SI4=@G%gs>{{;0K+79y%f>aMR%L!1Z z)!Ls@hNu3wnncl))Sw^rY_R@vDy|1(Rq~R1XreXaF{8Z$EOy-AD#V8+Tj*4ye(QOn=NoKoi6F&}C1(t#npLk&9DP$S9z&(ZH^;8ww)( z;sKNaMM&4WY?STv@Vb1XaX(u^J+w?++0y9)rL8}TaNDbLOHbLIf}XF&wQKvcnA_K? z4d(cRZUOZGzMGwFIiO;d{}Iq?s2pZ1W1PMIx>GZ#z9elTK-hT&iqcu!smc7VG{gXa zT6#&CTLuMMfe%g$b9y;~o=r0X)CUx|YnNk-f5)!v&_&tsiWtxmw$GMbM+GvwWvjE| z%=e6wV~ZKgMRB>-D%5%Q6ggPGX=&F@GK599(@iFI@BqduB>VvMGG<01-v^$@k=4`D z*dRp9-;aZx(hX!02^+1@6}rfdW_FF(+NX+7Y`1s7rse#r?uLwC2UybW#%8hj{OXw0t*xOFfLprh$XnHA^|_&HoWMOVkIF|bU#xvWZSj4e4_^-QND;5T{$ zF@V7j4ZC`=G9=LWx__eUjG{ANeuy_jhpM~L1`87Bwzsl2-*3#MO2)gSQ;`B#MRavDs?6^gHs1@W;o1xr+XG(pqm6DGB%%IsJcv(f}`uBd50^D zVa0arkux~VicOV9Oa$TO+Ue+we2QH^2Nub(#bY;v>1>*zgrg*}F8I}v49TJw2+4+nu6+ zfj{kK;{h2=O)d4%NOUM++{Oci@I9>+IQbHwEjZkYq(_PRvBZd0Hdx0ZE_*T(Kr7Ez zsSkM(lblbViuSQsB-FsF4Lw72Z~_HZxG0xlqc$-LU1=lZ>y;@hCmt}5i;J^e?b>qT zRWEpq_YYY%hh;k_?uir6I{>#iSm8J+~X;K_R zE+Lw)N1H>8(>>qgw|C{jruY!AyYXcg#1TM-Vo9Wypz19~Sg4Y)^x)kSN z$;jf3L&{gVIy7r6CoEva6J{mBKSTS!q&tedhV9EpC*SCi!w{A&)q{^mkN>n})C{o* z?snh4w}0Aq76IxNP4{XX@890uWZ{a9Tx(yq|78{adGbF8?4Oh5-ar16jzOdW%{-Y| z^X0Ex`3rRanV^J?ZU`rKz;Ky=Jo45*veGVVWDQ4mwi#{Syj*eWcj4w>3)M3=#bkt# z+Ia!jnDrYyh=DBaq|Ob!T8*_pWb?(B^hAp^zZ?OAdOG z7N4JeL9&kzhAT;}$4CW&LuPNa?Z!u|GM5pp;@_VFcU*~Qc+P#Nq_8H!N%l5RJfVj> zO_H8tVA!F<2Q%slUE!6FD%n{HOM#u@=^XH(HMA+TX6rA%_Gc-}yyU~bif5o~Kw;Pk z#I)3FK{3LOm+amj{kdl2gK~fHb{e;hZI<<2$GlpfEq*PSrD0coVa-WIFc(hMrc-xR zxf_J|mhWze*^Y2`K=<7HQUnewz`GWfRvCOM%*r_#%W+s7kw(C54k4!5b&JbRm zO9-YUY_+89g31yPxpeEu2@^H?Y1M?}vzCRCIBq{2_PSh23y60`h(>0Z=3>K#gh zs0=iKGPn&*j-OUnx@Q8;_Z{cpPdOH?)l3ng5z9$Gz|17!zZVQ?DWB;i$%gz zQdaDU>WJzm-%{TrIMSqKSeZi^%GKxXiNMQ&Z}mQvkv}V2`uL&J(@B}5*x>;>d0;ZD zO|YA--`qv;aS3}L?T5&l&u*l~4Y7o;dyS8aAReiPHDX*ztHWSbg;RwKcu}i&(wMbluv7Tr$MGSP^} zmo#n&`Nv$&g4Ysiky6HzkJcXTz1oi5Mt{L@eV@IN!;153p^+8gL`BMw9|O)DHXPMu zR$5!3|KgG2avI+=@$L>VLK^nqI;;2ENQIJGZJo|IIV`dt36~-?0DdO7V~XZfHVvUO({mk-md2 zdqsF-bx`{7-o;^58TWw{l=;Ju3BQNxYw-cmovzc&b@ha=e^Gsgf8G|m_HdPCEj`PM z`|A47jTcw0*mr=m?}fSEXcOe_1EWN@@@uy><9qbN38hE3>2O~e2bGAw`X&y)>`P;EilOEGc z6VCbA3}0zA&e7-{YghL*;!(71ef!lU;qYEYg9C(BivpIk`5tP)vuJyHgpRbW-*;7& z7aRlQJAbkyQXBlHVVE~04A-!Sp0eX@7*u~=)JaCzmTTOogVd=O^_%cN#EcUz6{<4h zYZ59n#PD2H_StjV-x|3mFX?g;4SC^bjvW;ER?)2mubc3ty;m74W++Jd8n>|hN#OA4 z1*8i!+_gv2c}BLxt-4UQdP; z)`aTQ(}BEpy!8El%7(X-jygl;q5_ckL%;0O&UljV5m zE-P~7cy1BfaGsNHuWUF&U@)oA><;k7A&{Ur!gt?)W>&gz=MEq|e)Y~vv^_b4uEe(S zd7pKOMde642T9-0)xG#;$sBBgbdp(Y)d729Li;(l$Px{8>jZAB9(BYEloNV?)aXV% zU{`DV3KumADslyW&dYC`d2O#tG?_H@Yo3s)J(xd5dA8@nFZ$>Zizp~5a`t`lv47jn zBA!QfoYNusqBZ$P7T(%eB!Ymz%d|dndw5l9Uht4hdeQx;%5mx0+J{Qw09*HLFGmHz zJj!qnQH44J3(qM1bG;xDu&`SQ|Hty>?6A%egt3IH`qI~kZuUS|GeAw7c$C+`^bBW& z*OQiy6(RvE?y0d~-#+Ne{G9KyYXg1o0!W$oqV~s06jbC2O@AfLqYW{L>h~FimY~AF zvU$C7whOPy=1n+sJf)AUAioN3j4gY&?Lcsyo=R%UwUnxogH)u^#b%%SEh;X+|043W z;Cyeb%#a8L$mVNVn;-Dpoczj6Q-feoC#gvB8$~@b5n=!dAkSSEHCIuRP##M7m zy&$0=9873VAf)aUWH!#F7;g8;kx#2sTI~+N?^cTCKRI8hGg0#~s74$Z*Ob_f8WgOT zk)p@(QvFIIPvK*%JJTgYja_Y1>`u z1iO@5jBuHOeV=3;>(UY0#)Y&q(+PENVu|J=TnAHMoIr^`)O`FTVt$IH7s5xD?P8QD zviYlAcvh*E(p;3A10z}B1$ybWhVK(yTFLuNi0yt=ba)w`zO;~LJ%PK&&9VkmXwE3R zlvWQ}ykO-Z;nL<_C%lG;=94;ZfHL*HL0QLGM;%U5FZ9n3>VcNpsNS|k3^p#q`}kUI z@b~~rW0n}=au)0U)1Vy$!+J745dQ<>*l$MbkxJykxh#9b_;U21dwJIe&RlTz^oM4k zol9xQzBVEHOV#s@kGQ{U`JH%U8;ZSl9FAM&ZTwe?KO&v**|x<__J}Fxbasb3rQ<^dGnAcuwa1nBRwd=Jm0I_G zao0HQ^VB)s9Un|u+bSq;2#qc;CzLWsmPlTu)6vh?$BA&^EM)M-25^E4)IOKQR9YcW z1m(}{1Ejt<87!3QqhVLoozf)=bsl8+0`=$&(r?p5&WmJq8z=|WV($Q+LVEM5J97=7 z*KZfo#-8g;RdRP@h-?v7zf%~m99G$}X=%3JlSQ&HY&{l{4Jf;o{fQj6m4sJDyQsQv zin4K(DS4Xq$ENwM){tbOP1r*{xYXKX7gu{5Y9t`Y;{`f;#et12Prw2D_u|> zCjy@1!Qq<_*Vw|z z4oHp$SDLQ*G{)Q|G%}?;`o7+}F5hT;D(6kR|ex(nPoupvUSr)Yp9mX+=z&o6K+A z0qkY+Fs9H2XY-RfO`W#^sHUImeA^Be4@b){ma1$hezkW5!+ zRIb*`deR4i-PrYyo^cO9l`<48DgWi|DO0jv?nrUyiI%Rm4bhvpxlti2&zRu{XBFvF zgCj(X0cY;4Hqt|a4`Jp`SzaL9~Zh$0J@hN`f94Gy#n?ryUfm z^cvJ0LPe20Kd6kO`<+r~TN~R1rx>D9k-Z&ufg*v}kv7|Te4vx?lunV_V&2+TL&+~O zKV6o8ljYMn(X{CF2o!zHkF5stYTk5cMyWl1t<5zm+G#G?q3Hd=k7jLFEyje>s@}nK z+>S0fSePm-&1I*`l0$S{_gVZlc8Zi1a$X<3JckT&joJ2#x2j~SU8!d*1<3y?!YOGK zAsr`Dm)JBu-Z(~}j=zZKE^NBBWa5oRlSi~>;goI3I0(UAbyA3&K|_}}aHLAnykT)L zjg2n4tm5IHh%~5EWpN9DfSTR1Jp2GeI;V(sDTe%dayJU?PcmP_g0LsM=8_W_VnjS` zBbp~=e0j0;ix`64_9|!v|FL%b_ny>$i*Jx+3*J%*U;56FeE{))T!{6}ziGirzQ0fk zBQu%$|Mb%$)x+lga%oxu2DF-|Je}GG3f~M8Lai zN`8+zdccI=mV~Tt{<+lMNn1ERzVS=(H=PX|*7&~9J7V+V>-J~z(K-rv*#)~BY_c{S ziknxV{(G`k3E!}94?e-CU*Fx&W%bQtx3~8=gpKffS z)9TibdX;c51A_3U3ymDVdKX0sYXB07NF!+`F&Qs8XYAJeZYOz5I|ra==rSS;1zrw= zlYId_qUWl=i2T01Qh=+t#_n_KF}T`hULN~-5miH2zfw1)Aw@m;1W|HTT}$EH!hnQ@8_(mX$w3V? zJk%qroOEJb44OGEo7_=}3S)PQn08ZJs8`W+pctObpf+*g2n9!A+x@v*l6} z0+S5{ReF&-MmO;9e$p+tvrmLa}O@IAFC$CfUrux0*K2>H%%`FN@4iI45ENg z5jUxjVg2HmeEM4%FuRmv*0lCS7b_y1l8=mLOBe+KU8&dM(Y+10ehZYKh8lktsjA+< z9`CU^m(quHTn5>JqfL4-4pn}x1NwL-r?&1Tt@as;;e8VwBe}WWbW%nwJj5+iwhDtM z5RFDp+F5n}6KO>;i}cCt50o)vmzpOeS@FkIK~J1SQ%~ilW`2y|uU0iWl6K7Xt8_v0 z*o5}$hkNul;t`3b*Ba@o&T&<&@$&V{i*lArhd^kp-kh*^4iByxPi=qhzI6r@iGl&K zqnRRxx6=9Pu-M$I`eR@X))pGkIi6@#~7?mv1meA~@x}t_LZkO4%`tw?P zP`B``8=G?blNB>-k)^|Gfrer%{TEF(?EYrU=#%T))`*vHC==-)v;5NSS-6Z67jr67 z6Cbo({iz+m04%Ir`O`SH;TP+qsiQDOLyyS>C2bJ(4;~v(2`k%R9U70LNX$(of=t?L z1?z1OSS7j&|BQpE^Kxch2-!No&zROwN1>q*eqXwP)VWT8$4uF>rU z!!#38c7>-~9*ZT|!JgeI-Mcn%dD|!6O`lU8U|TkTU0x}ejb48sz#Q@z#yn<)I+++U z{7xWZVUrZL3Jz^zsokt@&cM%RbHm^v9NkywJzM*6L1^}Bc8TE4lvxx?o(jw%3}yXv zVwBQKSbJm8fuB9UH#2%7-n%5y$J&DFSx5lmT66sZdP}EAR1op;GX@59i>S+1?Ah;< z;FoNiNxmy>?+lHD$v9GDk3669IzB}r`vqWy;`nhaHn6Y7iV;&`q1}gMq1@p z|5)a?;eF=J!7v&hYtY4pk zzlMZc^dgtC5a`EAD_w)r~caft~YG<2?kd- zmsgsN7jL*_hec6&X3uMGv>(O@>fV{5?=OtxSS-Zc#k5o{y)o7M&bhwjMw?(O7#_I~ zG&YOW(ErEE>&xzPTibI0@Y)0{Z*b7s3MHZn+ukUPybR#T$ZLp=o&GVQ4>k`^dk4|$ z3)*zMa5?xa%m;xg zL1u#-g_)1zUHaACSFF#)Fxb^>SX9E}In_^pa3 zJJl$@eA&!xK%E^&erQ8D5Z$X&Y(t}S*Zc^eA{kq@jjz(dL+JSuTL_w{?pAKG&bw4l z8q=?x6zXWbW0~H+(g!ZKB-3>>qGz*2)K$0{4;9W`NGt>s*=2ODRVB$2nZ!nEsZeJm zpgQ{~0S|2xXB3sj6E7d=tVFNh4zJ6YP3Dohb?S)Pi~YXiSOe(O(9v54WR*9PHV_`j znTfRs1G4vZ0b2OUiA0Q685U8sv84cnqljDY0PQ%ffq^2RxzQWIw5r(r0+*iKgs#h) zw>gwKPuQaSe5HRp<37Y{ZCmzvEHVP)$zFk2STjyVjR;lPrOEm&k04#L?Fy&x_mTMW z-}C15jt~_iO!mf;)qZb zVKXa()s2BXO91%N#p73xn{fy#q<#S<*mwz+7{Rv zQps89II&~6H^5;t%k6xigtLnLZjYlm&MxoEZ43T$F2P7%@NXl z)&s&NXLAc@DY`Xs?O)aGI>xo*jC@m`f=K;{TK*F5dQ3>#`aO+&zA$2WpR|;Sn2J5R zx5ssEB>bX=)LLT*B~eR)Bok5;XRneQNf*l<+Y{Gsef*Hvdo@eyRM(&@^;67KZKP7r zO3-&aXfyUE4A{Z0?noi^ckuuggd3wN^U~poeiX;_@ED6JLpEr-ro9F{85}zrH-9_m z&TXc$^1fH;4$k?KWvm%4k=E0)1INvSiOFi>*VEm$8;_4-jnZ2Q6gabKN4xLIdV)yLwiogjAu4F{#dpWfiXWVb6pEsXs6Q+$9Q)FH# zXhTPJp-#Ty*w>C&juLd!atd3Z{d=pwpJM#|Wupw1JeEA4%RS|W2%WCV(Lw6Oyr5W9 zVYL=w1{sM5q@ri_x5&=+TN<5d0A! z{na~)s)fI0e;xa{Va7kC(d54#BDl2(yexGIie`*n2M$q&7;98c9Xvl3*-N5kTxX3l zC<8C|SeMP#We&-OC7^H;4}lbM_q;EMH7 z?&{j3hUL%8P~B(a9@(V=>8sCL#+8&6`kocTNzw{Snn`eF5zVGwo zgOq*irXU#`!9bFVMMbX}y%_5kT%1e|vDJ+HL%2RL>xXXbeUaXp`ivcH-~sskk=Alu z2R*l!R*A{b?tCz4G8Az}`xB2>q-CUN|ABV#>F;~Gc6mBoX4jYm&AN)1?rUB&(cU6j zPQJ^tm&960$$xl`~LgE3xnk&^#gJM!oaqiXN{v?4UY$ zU&Mg2sNARQcUiCd@jaHaO@a#^AYNO~ zMe$<(J{;@PdAS;+h>?&VYikDy+8=*>a-M>-z>PHWfY*HyPWpYw6ZMh>F7gw_*gq-! zS;!5;pM-ct+xI_9sntjw4B!;-h>lcyzwl@*aj)vZF6hy-g2u+(RG&8$i6PH`dyDpC zYn28AiwM~5Nbau#0VS5(xz!y#e{0{xM30Pv07a>SqkpNw3;6%-i@#5!=hik2$9l&y zIkRUz@edcn{~#p!i^zIxg7)D}uea_|${$<$a9{g+2= z0dNs<;R*zzhx~Jxu5RQ+7hOX=h{Mj87UnD5g;WY%MKO2&F2)sKKz>e z!-6O7Ubk5JY2-o>_D*$J1G)99k*LhpslFFHqVgI@2-6!2nAH|aOY9IFMeE9 zuJoL06qVSa*jYiM^1kOJzZ8-uChrF*;2xs19Pk&$S}xVWy`sn^4Y0Z zU8|@uJyNCF2^)=yHBa;TA5y_R_hakVs&ReDEo0c*O7tImA`BX_`}#Z5oihjX|476h z`as-~NZ95SBdGtqyc{;n9i*~?pDm}m_7dY@&8+9RLay^#KWAlRF+i;`CM#!qOY z$(}-O?ex&6(l36u@-+Z3GkWFT-A<-;-u;C!Aj!LU8}AtYfCqV(K}}aIMEzfC zdf#&n+kBh)mRLjgZ7N~>C${!#7%)0SzR93B2y-0}?y!56sCvmG^Z*R)(*oGP=z11r zD_&=j@q59e;mu}k`oQS^xMagdZQFy<{>ywJ;tZ@qopZKPr!l}C;yUdPjIDKVJGZ8~ zY9+AW!10ap06jR{;YX^zOH69f$#h6!%R2~bIwmtA;Z%UetO4!rqhJ4+kJz&scJLwJ z;Mct3%WkuZh4*6ACYuRWBL3`+7mJyTVBiDv?<+6LFfK{wwrEK>9C?X0oQx z+nkZYR!{8%{0DwD*2DI|W4T1Z43c3LoH)=km2Yr``>nJy;?nja&&9xC^lIajRGi0z z$(kb-yGSwgcR@-4xP3vm(vBc7P-N@M_tE&}f4lUFn>S>7ID4ADJY2t7lvh(11HX23 ztQA8t$Pmv3W_Mn#HDs6A&O&Zp%UVe{%VID(dCk`HIS4l!gXsbfqWzEYMLoS1WjvqV z`bQ`Hpm}ad);}{QVh46?(z}wEUl<#dpkid+)jBk#kO*_}dp(FsNUPL|qCFt3+r_Ej z-zO&8HPD+Pv<+z`Ehzn$^_^^D1XEpO`lVM5EyPDksNs*N>EE|jfYbZChk(k5WX#*$d7JV#mx^}8XVYWDr`ZfJ=8FwDM7QawGD!LgE6?` zIXf4Q{-omLM?XdD;B zy+eucRQ7m7=G>rGt~O03If?o8Q4@>fd-zL;3N?ZIyBLfPA-s63_Pjc)4X<{=^svW{ zdLOIgc_W}6e|oS0ylEQ9*m5Gz8JNyT5+{g@wROD{bE;MSj}5sIkG3NIHnFSu;ibzV zmB%~><+eU)v$uv>t=v=7jl|L4XEYJr&S>{RFu-gf+Lw~BWkF#bPqWVMMAyG$-q{NWm9c~;*=)!f zB;{kUCTCIEn}K#sbGmF?&)lIN;>xVm>=87;uyt3nGoK8D*UNmrVx+Oj$gD{g{ghhP z%bc!4;1s3UTiZoyXKuSr>%wE26>0j%-DsHMA!zgU$jau3RM+1L^S1?b^nJaZf0+fP5#T3VDq|H~Y8m z4P5A<7!R6{2h{>XoXb58`p&< z?u_o&`s#w89&p=EyI>64`ylRr2@~QUKB(VqO!>`SHM=;pMs^^5Bact-&>#>8krnNx zx#4o9{;Yp*^=ujYy+Z%eM060;JU{3Gk?wi+J^WuPbf2kx^jI=J1<5)S+hKN)mk^S+3vc4&^qYOjRJGiTD???MCB9Ih zk6k$3kfh9AVp!I>j=Pw@EcshFIxz;zgb`R%whBB5%Vrl4Jng&?$lj$dvc{L1qqC~F z@dV3Yzi-&CBW`F?N?KdHBLqHvFc)qls9V_!&}wRgNa_{S_E=58F0s!*>6!78#m z7YS&n7&7?p4DX*@@BhQ6|IX>Z|M~As|FKa2!=mNcghHp^TnMi*2ts`lq7Jshn=1Sx z(!J%dp~0ZEvd`ZN{_r|Vo6+@x!;6L$6qA2+pM9Y_7ry1c zRX>zxQMfWo>C>^xIDJ3g4f3a|+uO%v!lqa=RD1S~@cw1jL_Mn5mx?(`0SdQnS|UZv z!K8~l2c2_#NtCCn;+D7J%Cy=Ho62ufTlXPxtZ>%jSP8SBkj&?eDxG?l)@@fc$Y(Fb zs?@FO=mU?4+&W@o7mI{CRqAC-Z9cI_5fs1hf>2Jwk}1|PXXDiv^bBAa*sh6>*{|$y z_$g-DP6MsUjT?8=Yp-}37>>AYI_NkX1`UkQGSJt?EiKL8_>4F{Hg#YAkZI`oaWXBu z0rePmb;F!Zv)uc1zVU#{(abk4BHkbc3~&_Oa_e@0)1)n4MrqBTlrj z|4%@Q|DBx&cXhR}B(FTBbXc7wRwgqnX@9#vd1W{|^2U1yshc`MkvC{zKVk}SRb8-_Tn?a*@14ip?*$XZ$noU$RJa(!Tekj(1B;@wz;j7>VtKUa{PeS8j8*||b0=G6M zuc#Y5j284Aa_vXh15yjIx6Y;O`n4}2y*9Ywo;o96@uR~z53o`}onrvkPNES` z%!Q79e_H{dXE5hFpPo;>$&7wX-HyKm*$HKrNiA!mSTojm5;y=Hf0qv_JxrxJ+gr0< zF-d*Ok)b0I79Ep>(jVTa=W#f$^=^D9)z69>?oN#}yx1HE(D#UYGIjm_X8l5U-3gco z8{Ui;tE(vLn`(P6<)31Mlh%do95(|-EC%Bm9sS-Nk4$W;;3Dv)?-TnROp7G(C0&Ds zc02WfZni?B;nz|QjU2xN%oAL2 z{sgZZd4{=Vo>NjGD$;x)bEA)GSsJJ5G?Be}WuYy1hVSP~!<*sd@b4B7t4zu%cLjkZ zqfzf4CaG~2`?KRSU8kwjF75yk-{_b6C2vy*3H+8*Hr%0WmLG3#;>?5^T_tRMq;Bwd zOYXMsl`lGs_)TSWIjy~1MT?49xcXLA-bRK**DqHcylpA;v6GI!w~ti6nYc|!3wN@3 zn3|Z9AJF|1=|?uv!YM8wS_SLdWte{$w}o#rc8=3jR!MDfB(&}a?`f?A|Da`!U3iY! z!1y9wtlk$kQ*Lu~nJq(hkK%2$ehS~3kGB_<)=6ZXAaW`g^(AshDnDNc>ztw+f(ZAB zao7o0+q@F5sJNQJ-JZXyuQ^SETV&KnT+Ku2B!TA(+y3hB@ZS7dhygesT1FuYrF3w!_ z42hr?4>v^{$%v01N8EjR9wyeLMEB9lMXyN2Hmgv$#qZ`jD;S;yF+qVa21SuM4|~<{r_TO6uSE@W z@ueQFs`X5VBr-(@F?KQVY5u|hNq*=RL-p8|)3N`Dpzn%{ee?VDvNAd*Oj@Z-Q^1Q; z1HHB#-YACj#a9O|^p81ZXrBTMF;A^DCpn2+i3ae!^{{vA@Czuu3$I07e_Tkq4T0Bv z_V>~D0?JJx(4wokS3HITG89&yg3%kZDvzhGREz>Zyk#p;;af)$9+po1W;ej&a)Pr; zcWlHX6@3;JK&u;ReQJ)ae`z1~QP!nNg(l^=L!n*v@40O;Kd$9Cur&bQ4~^TK{k>RC zfO?ky$#XoJ8eQDX&642YBiK;1qW(d+(${EwN_1+dIHc=RT=c~X{#6Dv#+ofh$*j1x%~R?Ko7g1`m=-Wyjt?+{$E^Cbz?DeC(4z7R~1MXj+CoHy^{;+m0<5t>M#b z^~h3R4thP`aKhvY<^~-Vq(x2F_5xYo5s_}l*A>f5F1N!( zqf9M2pSjpfPawcn>@Eazml%wO>7@n4*#`VtPQdIRFHIT{1W5V1zPVbR&H6^W;lox$ zz9B(*eO?Cxo-?O(zXK7WT~1v_dImIAaMek%hO_5J2(f{Yk{#DwW{~QR^1PobjUj|d zKg}Iq+ZtIl(OF+64-GCYQ$H68gD+T;&*SQnI-ft4rfW#((C=5_kzpF7^m}i^)w1Ue zO$dvvbrdBe^Xv22j6Tek>2jUvQsL3`yVK!QkpNJau`yBc2^!7Nv1DZpdvk*xaJzRV zLl(E9xBh5H51+jG<0l}VxWv`jDhjB zMSiTt&B+1rGp72uf|;=~c-ek`&8zFzQ_;K;AK>zg@81~s3(mgVT%44C!=qe5!}Frc z+6<1e!Lx==a0)cl5bkGdx$^PH`T2c1!I*qN2m^dlDJ3H8J}kx5KQ-d^-k;A%>`8QS z(rmP|O>r^t z$`F0Kswtw-RJjg|aKlSyRtaBGFjLS`Wz&b4)cSaoxAh67FteTr|FE6ypHR7hEd(u^ zEjYJvh8dpb$94@nSZ>q3*Zw2C2X%Xg8a*kPPme@n4a0Btzt5@iv#_2oGdB}(^FU_p z6T#Bzekc~#veS1bZRIL#`G*H0aVv?9Q!mp11`3GX%wdhD%0&*G4ia~{sXz_9 zJ)IjP@@Vsh_(2=pS!aKF5UHP8s!*$sdOqxKbj6y*%|tjo zikT4Jo+b-u&T6|2i}$VT@_Kn6i)iXUo@|HAG;xEC&pM(V+7Q$imhgc|)#t z-0^5kU-l4G!>c(zM3<8`BLO%&HC*9>@m))oA#+Ki`1^+tK*jiEv~SHU*AO^YSlL4o zTM;(Iz)TE(ZG`Xp#$6V6QBEvuyV71n;HZw7S1^}GvZki2Xlg=PAsxgy_P_391%%1AKy@~1L_!AbPZvAV|R>wc$n7Kq?BRq&4UJxI!O zV2MMNnH#DtK#-^hW{}URhEdN$vJV`0MGF+vNsizQeNT~5r4Z31;!O65I1YN7a|kVL zqwl3#_FWukAjG7+gz@N%M_89I{nPrWW*R-1>P|5^Vd~A zGRsCcmc&v~q0=%s^6z1uZdMzZO9#I;>V8aJ^?8`D$P^Cnd8;v1il~X%uh@ZX^({?e z_I;FXq3li@`WTT!}y-Kq0d!Fnc{_i#b(_+(Wj9X zwpuK_PKPV~g#pE9p5&G=s(#%!#Oknc(dNCz-f(3QtQv>WqbV5+tZ`QF2F{@B+~}E{ z2_UlWvC!s;+zut@(d&{o+cxHxjHIR!jUpB5uXx^Nq{dYC^#IY8RYeSn_ysvL(!( zVnxzEBtOknlP6`)BETbQK|J}0H|M*ShOt!xcG6TGD@zNX?WD6L?;NC!n=5JddGfmt z$LqQijPrzbMu}>kK;Hcb?U{>%-OA3#PWB&;z*i6TJ5`?8mF>IXC< zUk>DlVPi8FZXqvo83#v_`5^nP(*jU0uWv!Qdh$J!`1Q zx5G;_c<8~ac3A|KH{gg@4h!%wwZ1nhg&(5aOgcNyg~t!x`DS8bz44@Df%N){c3w@5 zWVBO$*x2}v&UqJWR$&7*4|c2+73DSCS5udsn=wQOB+l~ignSusvBDSHs?N`k%NA%s z#b(FE%jCK2;-(RSpa-CKvyDar=QV+p9fMZFr^gZ(sn*!_A|I>WaYxe3BvC2j#D$Hn zGz^T^i~>8%X#xE_P-1qy*mQ~hw<;d_RKIEH@$3F|Gj}Lt z5kH46b6Ri8hi#Y@h_%B|DeUJtE-fG-351%g5Of`-LD9MM8x7w$twLjL!K(0ejM{6- znX`kopbypbbodA%Ugv%N3>dm)4jMx-k5yQ;T#q^I|DqAA1a`dtXucu1O526r=-fJg z$h2GYbk&mg(_0yn7jFEszhW?2i{d5X-C<8hA4udW1o68Pbb`eI2_eF|oa7Mzsn-Lz zkqZ|MqQBSMc09llm&)Y~oWS%NqNbb*Fql0`q&i=)`Zdbz*o2tlw02xb0+9HqIP7Qk z7b;O09fo>Llmu@3i(v#+m{i9>1BaUw2qA4?_~#Z^++(9X*}(wW+n3bLF55B*tz3`d z8--moQc4&H5$*dr*fp{Unm)<1yuy9QBjQz5>I(EgU0Rxhn>_P}MTbTFI&_9@#zjGq zmzh(h?6HOE5O9>AZxBKwq0wC)#7Be~OTfKBhkYRzNj$dZX7vV&?Bpj~WgGk%@9&AR zRp*-f!%)CmJ+Jm{GOl&5+F{g@dDioZGOf)=#h$?6BP2>?_4;So znv(=ek;g`MP679)=E&&rU$d18T1^>kxuGV3N(Pbk%y@< zsMpg>*sU>>58K?`RJ*Q-9G&Lki7gJDpE(u+v}>UEiYu1ypH*6?_)ZTUgZ~U9TMO}5 zc`&GRxWgxVn_xe8_K$Y;6W6}RNT~sq!mKKc5DD;;FPXU2S}e31J@im;UaMP6Qro6+ z@U%al^(wu-LOD>MYWHMiw{z;z7|iC04IRTa_XEAx_UGOqXOUM7>??|;8ztdbVTk38 zI_}q3Z^A%z2;o@IFYbK|4z(oXzA*f^xl1>fw=Ew0Wm((=XBn`T-)7zdum8eer-V32 z%qmL1VH9=>*~Vft6z(DFUGoZ|QI|4ri2Ys;;nL2^+2>j(!({b|k1HE@Cu;UAbpcTt zHsn&d=(jN?B{p+jmQKc*$V%jWSLgJOWX?uS)qbvWR>Q~C{nS{<(fziIJC5}AAZ%N4 zqkH=a<_t83t?!NJs8epvTHV>@BMyC&=o)9iNazQu=e^YQa&~cFCuuzMSuzUf$~`gS zbqdl*YMn>O9(ogXc-L9W&Q(`O9_k*(s1?svAOxuoIf6fF^6}H)(&yb{Gl==51EErG<7Q4#x?aUeF^yY#y?&}k@b1exBbYO#Y*yH%>VwIIl3%G2z#-UkkNSbfvAdBy)r|TOBN4ybmZ(V=Jd*k~KcQVn*(4&m zx4v*~k@?S);y}U25|c%M8^E6Rp(m9{s*i8B1`mkc#K=GMZL6cVFqflsNUkrj%1B#l zD6WGv#<%2}7@+D6k4C#t^0eTe^y~-pR8E3e=z{t#)<jsUw1O#e# zY}m7H?Fxf3a6H$iHL&QZ#ws(|-n^Kcn)c9eg^?rII#?>sQ~gST#C%~XTh$xI#D#iC zMQpM6G;DS|ZScy-bcha2MBk_C&BIpsZXv8gyMnu8psUGTW1{*O*#rLIoO*mrNb_MX z_}D%XJVN>ng;#N3Er-#{tIOR=lGT@1?YVxu*nU(?lNIjZsdiNg`)Geq!eq{}O zPiY%0htoAK1rH{4P$!M+k5s(S4VS-_u@CBD%)xco(g69TGjy{Ed2{C5$V38-l5;mEMR2QuNq29axy?J`t446o* zpDwIAu2o2B9^2wlS87>aKLI{O2lOMN3KYmk;v^sgB40h23hsTIgJ)heq%LT&_;wi5 zl+WTn$3TnR^*8hQW9afTTS8_pe4TeF)`JVc^~s?`e_SX-pIC4n$;`ShEA*HS)*G}g zK9@I6;LSijqJ_X-tg9AXPf3b(6{xKSajq_0oGn`i(T=$?@O2vwm|FFkMl%NDXpyrf zyy+Wy7GF=;!qa*d)?qJc|+8Nf{~Kh#u!8VWWRO**$@HovlRhU%;1d3N;Meo?aw$OmSZ_ro2Smr zjrMl~f|_y$ZRWMp3I4hnj5Og{$+C=<TE}uA< zSI_MYOPpEa#@>8C2G-(ibT-n$xQp@-U1E|_ZT0qYy#}8}GKXw@oc6*;uKNnc| zwR>?aRg&<{SfK6`NX$vR#jbo*hZ!imPRN7Rtvg4Tk58z7l@rxgkA953{o|l?rGhV< zSvd#hLQG+e#a-Xkt0{|Kh4^JsGu+!;NaxP(PI+X+UCT$clOJWD?QQTU1A}MF`s^bK zYcR>0c4xhVoG0e+y~FdsvyJ9`r=Cw*osl_VT&jBY>&q>W#xrw4k>~iL^)0|g*Ngpm zClG{@=BM5cpS<+}$>poG-GK*c{BgJ8J#dpxHE#2TD(eoi&+MGZ!OF^OvOFm__M&E* z-;He8PsCLduiuiAB?ZNjemea{;vG(3K-*olmtowrR_IZBw09d@R|6euXggv7AKH6y zlG;dBlk&4?#2{cxy3iBkVdw>K-Q2AHYvFX^4H~pRECf>V0#eMZc&Bsq0#7Tv7DrY92yC4*a}*}!3UEpe=WlOXw;YCQw% z3(?-*pv`lhfEk#(=;k^1zG)1fpPY?^`ir?(KjW(%!%9hh0VT84m#UylfkXN!Jc-+m zSmL!14c(CM$oPSlO-M7&>z7{w6doIMI42p!kU|;qr*Vp6U7Kwh{7tDIP7&t;dpbQ1 zX?8{xvmVg8&>Y19?){1R3L?lyz)hS%&EX;q7#M)Ehg0@?QbhK@#WHlQ4e6;?Y`Zz{#vlm^eUGARHR9!7AZzL-< zUCF;u`!aZl)sLMO?w29Wo&4(x6`X8?<;yh`PPol$@rkPE4ce`gPlxut33L- zXuo>W!?$tEt=zAfsz0oVuUVC@9Uaug_3Kf_XpBtwX+GMcY)_73Zv-^7qZ`y$+V6jx z4hkqAnx?TRD<`8>Vl*bfA)e>9Ro=Tf-=8uynXa1n{&rm5F^rni#Tkb#`iHq;wJ9{n z;vUS_oZq+X|$pnV2xx1v{ z65Fv1fImOyiB7+k>eCrnr=lw1ja$5t?jJaqzm17dv1;WObUbFDn6BSFvRQnR%6i>EGaV2bjs zNG%q*aSb&Ody^ZUNV6^JW-SFz?IQ7gVvb!K;~kq`&75FbM;CUJfi-mr4}^gg8j|s2 z!;YIhr3>sJ&VsHkKh!1B>O;aNCA&;s&T>X?!~hXUNJ!>_L!(`n)#sh5@@Ev%|5{Lg zb9t8H@@-SoYCMQLRh~S3Yo3M(GO#hxlzAXu#eKXO$h|6xnG6Hby{%o?(WeBlDgMO! zK24MKQ#T5vg5UnsHKGPktM0#NntGS8mD|7AHNS3kV&#$msyid6tW z4@}m&Vmtrk#5EDag!!gUM}UsHakEXvVhp0oio zf6-@5PUC+{86N6fF=9>k9>u9^>6F`(CYmlsPHS|LfM~@;L!t3f5cVfOr{~uB-6-B5 z-%W_)4qt&IzsS;hi)<1rh7%GC{#l56T4~@E{xGFU?QdDj8gnU!t)O$k#R~NXPNZnc z7jUAi0Vbgg`Y~jEb3>j*tM)R%Sy}6x5pX*8TJ?`!XiqtRjRSpmA|7wiL_0U&%2Q77 zf6ACRg>PMI!@0cHH@F)cG$r@!jUpSQCA&{3wW&4E*#mv^^r%}>GH|Cli5<2R{uDjaaf#6Cy|MKkD9m!4lkni#!n`=2=)0kq--h7*w9S zr@w8OS>B&Vm~H}26pa;wn;``KM-c4o_Bqd`GB0{c6&ZU2v`<`}q((;WWW1Q( zW1RW^UHsrAj;;(N6SaEjz1KrJ@~W8C!h6ZU?cz#1Zs$dZkPQ+YaiIS(Gf ze984|*7)~BE7E0Yeu0ZNB}UheM_*-N`qN`H*u}pF< z2oexo>-+5AN*M|GiGu06gpN}S6TbXfPiRVfa~Um0yF6dPLwqn_L`cY63lxmteR!qq zKlBc;J@4zEa;p_rXxI^pVb56doVI1De(L#;g5_QeR70C)nSNHX*GEA96BR+`7~lor zhHnth4d%?W^Csr{Ny2&#NhKCyIO*#^Ni1A|ua?rbk@Tz$aq0`PeQ7Lu1HUEz@)w57 zV(^@4i7Ee02Bt-#n-D)g%`uU{qDI+ zxb`-)%a-7 zC)}Mv?VlY9U5_18zFecPH0O3cC#k3AVgO0mogHakkArAZUzwb>5c*iy~+9kcz`xYq(l5zP3L7 z`HU%6Y{al8W^3vopx-a1GOS@F%_%q&M2+lQ8<&(=}qq zV%#>0I57g`bdGrphcXQ!y_|w3Dwog5$lWExTBo{b8IB6|F zK~UI4SEycr@Pxr(3z9NBO*`e!RQk7z|2D$YosMiyc8bk{&kiCf7Uv-D62WZ#5oa(4 z2Hawk1YH?E0M*Z{6I@9B?1R#yh_I3m+IssepYiGW!#jhKKtIwt^4X3Q~9CWefSZ zJp9EYNruKaE{be&fU8D;QmEOlFk>If1|;vh8(jH!fc!f`aZceoD`-WyHrRHJ1}j^b zH0W3h)$M|Fh6!WdDcx z9J4;TA#9ksni-k<^78-d_i8Zq z*$>Bz3cn-72pC4cQPn8c;hMX|_&k5vi1XyC|D}laROHWbb#vsbS!67a-zQ%zIq|bA zpV5CubV%o6udX=PK-l*kiX~_BlzmItq&A&vHH^lc8aei>@?6tk0n(#X5LS4UO5B&x zD~5Z$5W2x}qYGeI?V~+td~nFWJmBl4B6ycitz=(M5Q#aLq}nS~yQ4Ze}@M&bSb2U$RwpxW12j$KUh1*R&;5(v0c~k2$Hn|}=sfubtM(W3=|K(Dc?xz#I zZ+3mI=?wgLFBzYW4bjJ;>GOA89B<&W=$XzW{P@@mQR$ixmrBxE$*v1E9&U__gY8EW z4miI570TWJ6PI-#07?L=$6!Id?Y*W+t|3j^O^7UH)zXozh+*kWDxfEgaqQkgjUrr( z=Bq{O>oL+g5>Iv}FWAuS%}(0R|03?aqng~lc2N)&lqR6G5Q;RF-aCjWpfs`2d*}%@ zRO!730@9m`h)ORZK@{;bON5f5N=}>Y$Ll`pxq&~O!n@&zwtNxqr{8`0S4bL8UuZ15i#NjE zY`-r+WZ{#V2z#g_IK}zY0kGkBboj-mm{^Ls4;I?TZGH5BoSMqR*@1i8<=u)*8}`6| z0CqgpXZhl$U5i9$)`Mf z{nk_C<4M*Jo&=b`)125OMp_2TtkB9?sr>wRQw$Hk90X+`+xxNYj*R)j36-+bTHJ*> z(^EpsNACF=gt{UIa{NAGIUn#kF(mOvCU>5+k$LG}=rNIL`SY#NFTedupc<^XZdD9| zmM|Q2SU}QUj4u83$a6Sc&UC#=W77EcM_exNzBp0=u*f_P%iO|`n)!OE&mAg1{oR>6 zXXLFX0C}T+Ds85(lgTAxRx}~J`pFF41CMS{QSpzpDIyXtq6eyFVmc?jPJHTpcDI!G zb(QK;KXZU2tpf0v6*Li=^1DZZQ62IOl!Ek^Lza6ltVQ~f^LP(L18KIJj>W3CxHSTu zv0gXUcv#l&>g-387%+99!lycqGiBNBwc_noACpdl932F2g=YV&#jv*v%)jE~ zYYK`Cv{^_pnPxmSh%2Mfy$*qurIelAZTFXmU6i}MTd@LB$wt<8(tX~l3kr}JIh2V! zTw#2uto(bx43AoOBEZx4HtArW2WXUeDAg=qAuwm)&g|Tu-7~?m$9MNMiwL!<~?OsKLiZfdhYPovi>`lbgyo!@2%Ig zA-_F|bAJ)-*pwn_@X^iHIc!41k5+1~_FImAFlNCb`gKd-^j46Nd#u+xTl7kc9)-7>TIZ3}`S7wES+y4sN5WPQ$LVcXI?+vItHgI}| z$3n*P1}quG!T?hs>v}9f_wJ1N-u-{ah8j#rRv>RSrI~8|unw0??ow(z=*9|s_KiJQ zBXNShPo^#H%WiHNYv(gj+DBdLJgyL8tcO$JpjgciNt4aB|H?oA3R-%NQBuIDC7F08%djUEgRYF;vLc!Zvu4dJ=Ylup@; z(&Q=AQ06_p#y5f*g7)~#{w^u}-y_FsrCBuV)p-q!rQm#G67_K-WO9%qA;!mTao4E`60oWr|%^JLmdu(r&JyRHfbs9aEQP zAMWKQB#INz;;euIR-Ncd4_@dLWwGwVy>^l?lbzAh?dI5On>xLmjI({qb;4EYi)D`~ zAVy-bFK<=+`mL_hhsZv)N@evY4*4N_B9mXP>Qx?@Ds(;fdzYVEp}F)9C+Br|3;@Q) z5x1w<@Q>3@mK)V%(u49jdnWust9*ae{7wwpskL66t*r}Gxk~ZYLUUNOZ_PzP-uS>Pv zC>oXE(y&+nl_xa0ki=Acmp*uVcH_dbLc_BuwO0-Lex>Ozk5QibC3Qq4yeRnSoI28r zM|BY1lpPzt2W~D5KDmCj29ov&rEQCdW1ZZ*1m;QzseGt6kNvXqFr;u<#7 zVFQf5ilt^LBS8x94WgxI*D_O`e(6(Hf9Vt&W`ThXyCBTI}TM5vbbi!R>{F-A09i*99x~ zoD+NV0a1>bW0x%X4?J6>Y|_W4tstL;|O;uW?~)(MVvU620g9NydfY8xn(xo z>#=od8jD?k3@9p6#0fZV`t18FUHkfW-VdW^so0=xzpo(mR4lPt%cQvUTt@NLdJvUP zD(S2Fd%MDx72_Pvq+OEhFzv1vt+ZnaWe%2+1ld1{ey^b=>{RVr&$6NHA9658q0!i8 zt@?WpaQ^Qettt%gA6$NR7`JmVZO31@EcEOZ8UJ$k|14`qgk|MXyx(S!PPUQIZFx&} zCi3Zv7nrC9F+S6tDl+zMW9OJj(?XZ%x`G?=SQPa&<~yeRt9SUgY>Y~w%Skf*&}{5s zjnvA#0yy*V&#^a&jxJGA(22{tqfBcRqBfN47E3xZFWFJa@K7FtIN?l({D*idJ>h@^ zd;81Si?T1;ftIBBhWMVPB0A;R9}@ z40w^@g|g+3&4V{ZkFMToXwG5dBgq?DWsoR(Oi)d8Dr^1(imB_V>(7(#6r#|JsRg|i zy)qA$dRv$OkX9x@032}mp-$~jgBD9hTFojptvRMw$qoJT#b~m6K@q3h_#Yxgpoy@O z1?X*I8CK>_6BI0Y6Y^v-+3;sfahv#7QWr)cJao99BflpQ7d zxnV%`LdPIWQTAK$#yy4BD@6OlvU?k!6@Gh>V339k<6geHZ?>x;>>Cxl6lO5XijgP* zt443AMy8Gakj;i>PN&}nRY~LAyo^~YYo-&btB1gE6oC7i4ddU(B9H1Ze|G=NUxp!X zp{(UEzk;ubuYXp$J$YxVLtw(1A~gYKD^1!MxdE+N%eyR0KX8KycI{H^nRSdi^_zTW zcgjiiG5bNj#O~g-(CScB&%C)(zx}&Wssc3v1H0!@L!& zU;u}NzsnIRoz2f$(@bwrDCR+x(K6 zwh^6^_hwombz$7~U7!-%HM<^oF5IA)%L(r_xf;5Xf~0!x-5=m*QXtiVqxkmveFNj< zVST)?UNH+z1mP#p|bjsR3Q#@w+z0sxOMNCnt<2TRnlGGAB zybRvTYW8SEszO17F1XW2R9-E)_Kv<7b#+8$aDJ|!y6V`Ee_}n+0%2dt!USFiDzR>rxG%pCUCTr0$Ea`KcViYH zg7-h1%Nlj-dQ^00@ry0r(-*nD;h>eS+UH#o**r2F;Kc7^=-@rq6(wVE{$D}x?^#_t ze;MuXPmJD)kEA!;-KsgGQAsW3n`Awr|52Kh4k5vzj7}5c_zoOCY=)HXzgOPr(K8+Wkhd=ydGz?S&n^5 z5}8HSWWlD^3>z!{A&Rvla6Es9WO&V$ON`o<392^Bo_Em>*VeagT=BYbb;G67Wbk)K zNmWiChNf=um1oP`gf5gb6B3$?8F{B4!t$~L`!z6qN_gq|WXZ`75f2}wgdby<*jJ&p zt<%;9XHHpQ5y?Jku|zo-nox2YSUE&g#`k->4qd*KTmfZYWG;kMmc{}x7taRKOgC@) z2%U-8iBNd&yFKOidR!s`s;~E?o1057?)f4I)9q>5zt+?6r!W3(8Be7Gv%3qY1PC|N z+hWHXU-6la#_(A})m4x*r9aAY%3$V1nv#Ld?3*!WRoGuQx@{sfXT-*`iCkzVRBT$(!8avqZFj(gjYD6dSbDoF(+Ysu%Qy%6q>x2?1-ZXd(bROe8pg9 zU_3ydW-9wc(CxQVguQbu$?$g>@tklp2f1!DeAPo*PM6}7nLr5xitjYC=a6}4_kO9+ zsOI@`>kGRc3wCNY$3xBIzoogatjX%hOZ`kwA`hM*`tkhqx085_q`%jR_MuftLMux= zkje(N4zy7^kYym=?cb8Cv)_K4n-F@3moBwPCsbIGM;#xa%@Od04;p%zt_{pH-F{q? zZ~}S2@Ox7uUu6Z6E%F!L6622V2058|MwQCbGy>*K_o&Sn4p0c{%yLaM9?_8oi-_(uQM*I?` zWZ?9n@^EdSZ*e(*)vQon3E=)v`Ug=lAqO8dJgui5n;ZS!MYt5l~t(Lb1)ZFPA2AL|U3ozGTyTaog5$ z9HG}(*8f_79wK^O}u3psc?9AObM8csw8M4I%PPx zX$kF!8rT%6jRKd!CS$oLCkiu-sCPea**WN)>ND^y7pIM&n~PvSiF)i{A&Hq~ihe#r zip`VzuXPH*p6~jW!A8}0RFb-Ig_JS5-QCN6;4NX*>AJAXIN^y2Cm?!GE;aPKH#?Ia} z7mwZ#H=pV)R^JxzTl5x&4Att<9U)pn72D}m!7|^#?^7oSflMoL4DP`Zq#0VPn|>NME||>2^mNeOb0aSRDKiAu+URA5j*oq|0e+> z^%R`?;)l;@8tTU!8*gFyA}}}jwtmgIY7fENuBkt7%$iV^>OxJ z$?!H=r#`i}RL4+bp9a-pKXlo-Va&{gUH0sq>?6hbqY{^bwaTMXn~&XUkH)kR^=s(l z%HdE$%>=4dH>k4reK(@IEB@c5I+Y4Xzs)G@yPqOA#}U4N2YWAP9mZLq1uFkq=5iHM zcSOwB{^4s5CR0RG7WsDED zs87|H_EH@T;}PBe3Ui)YLP#|r`b6G8{d14r;bg^&@<*fmjP~O{S>FpxP`RbN@rohf zgN!MvcYa;6Eg97$QoepKp@)gTy%_^BPN?ou$@A@58M;~%?A=5uUdH)*0fjZmMMq4z zt!&s4*!68qLnI&|%G#*RMw$4|q1nFp23K#v8sa${{RI;_w{y;=T^R`3;Bv;VxDp%o zkZU3CXTr<}RSETatXZWH8&n>xX=jaK*7nQyEilOorG z1B^E6R%7fcd;V;is?Hb;Y?X<(yKN`J;0AA1vi@G`#YaodvN?+D*2tQDaj_2`>;nrC zXfMK@#(Hb9{0SWN#NRS$x*t!>4y`bVvV-Py2UY@myxq5E2SonqZRiwiaai@OL=^ST zMF_z6P3`^t?rj7nyI#+kY_XYbw=Sz*6Y_1@%T{hgN4wv1J&LqOj8ILJ$OX!-a6KJ3 z+H@bJ9R#IxR8K4e9}M2w!I})xKXSv#d|fVauX;0H00|XdT&q&Z05uW?W%z-(2}+OdqZrDKY4BkCJkN9fiuC4)&17{AcI|2a!LIcN&>s zp@cih3CI(x2VxcaC9Az7L#F*k*I|9gzC+ZJSs;iQh5sBH%pr&Ml7zEap*|xrm+G^9nHm9yRBf1aHnoJbqjT zKs8rhw|ykwebddu;HHmjw@pB-;r-GU<@cXt?pnXOx;?V7EmF>{{g|Q8#PIcTWw;G= zM8)hWj|VoEk&P~AM!Yg%f-5N7nFWn>k@&b|(!}5SvuZe@bGMZ6DmX88+z;18nK_TF6evu=<>>iIk}6U#Xau8~W1ULfpd30>d%S8NuVbLFA)ggZRY z9-X*wt?YS72l8Hyz`3xKUan;NC!vp|3|AA{USo8sh(na=~xc2)OI>T^t| zkwacF6P({}^P7{~ilz&W73yRjq~R*6TEF$GvE|PbU9(xa6Q_C{ z-h8+UkD!c=pbY)qW6s4gx`Cx)!!IxIgTzHc0eVa%b_rNN4V76H-z?K6T1ghqrU#q+ z1$DNa4LTSL_i}1Lntf+lC}|2SRrIAXnS_t#lj#|>K7h_TL%4g(7XUCCUeU;TNK&`Y z=cW@{HMkgCr?FcbU){A1w5wATAvao;eYld$LSnGZIh`QTLNV86w=aqBUfhw`RCzLc zrMP7$)iyu3;U}xx+}jpo*Q>!d>!oBO3iIcE_Vjk0O72Y;Br)QYW03~kGgruKNTuax z+O|~Nse(G>*~}SY(6;Zo4f09hsl!b9&(*~Ji%<=wkI2|*=hR))-t3z>?ToRVar((6 z^v|RsHlDnO8)9l{#1jc+=X6Rx5g+qr2OVbuIIMl8jrcu_4NlzY-Ca;a?=7o4U!d+n zo_Dc~s3pk)2&MP#HPbfE2s5^Y>FbIeVi+S5uc-wesHv?J?G0cI=qlul%vHI}j?4}i z!DLR-r`}=b;6Gfs9q-}-v1!|K0Zm__KaLDc#CN-HoHWLF$%bqY4mGhR&5u@^rri7! zC`i(~H!%TGRKntn<&S3FKJ7Iifho^-Aiexf`}52yLSn@uESUeX&_k3?7Q26 z@tgN~91ggwy;@ec0R#37hU+J5wAU|(!Uk_zRGjpz4P}ct)*5ZJlUX^jeiZP_?Va^L zBnHUtIgE|b%SG(r_!EA<;;XeAaii<`4!)bHjmN<|#IN|*s(wd@h-kh?!v;WAb;=!* z8VeAk^)0)8Q)tP4#wq=l=6VZkuDMuxnF&*i#d0%wl^{68mQ(xoB^a2VLe zjY3wC3V3!}8>Va=UhU;Rwp%eW^EIAU2P^IblgxGKdbFj*YFW^H#%y?UNgGF+i5cph z+W|U?Oy%x(tTN7SZibYQ+96#yjv^{sUw~a%^wGQ=3AWna-?DhK;k8WRHu&9`7z!>= zX8!{Xs+b|Ox}y?bXO4w)PnI&#{-lL<6_0` zRQukm?r(0R9Gm7Z8N1vcy&BBA^@3S_Z#GGH@0_WF(cdny2V@;uAwbQ&DHx6VdB}JJ zD)n=P7$WA+X?`O{)Ewo%91ihYjAAu6;#GK$P1sLQ)X)Zus*_eBb_cZA;$obJQ^J?K ztox8wH5-zUt{*DFBK!MMq0CFQ3HGI9I?)klhMM^J3#lD!BTd<~U7U+_g$3o0UjZA^ z8c68g`gjdXTtr2bTyiq$;DTe@)wGfc{#BaCT^qR&NXn;>W_+u_0B86E!v$$5vTPZ5 zI|X~ghTkQICVH~no_b6Gm1y`y>TWkQGQ++jD%HXaaA3_h^$ zpV43}iO{}R7~i2{tzmWFJs}bP9-W_cW!|)(G>J63LRq(MXY8ds1VwEyNa-%K?1!UM z6On(HE=>h0_!Rnd+^Mw8J4(cd>Op;M|FTfq9iR6;9^zB{5!(rg0sulyx(?PSkQY9m9S$F4jk@I?_nOLEUykpjF~REICd z8Bx7L#_NbmUMPvYdw5SwD>h#5g2xcrQR%+@WO{htMXjF_zF{gczFU5(@>3_P3(28G zZ4ku?hv`KFJ%_qyZR@;vpJe0ry$)!@sdkvvKvW{yAUJd zX_RR)m=?Luj2ky#B!Bpi_C5>Yi=@`b~8~9@g>@MDRX@34}XW8{w$z~|DX82XNidaW!mi~Hb z8CQa|_ZX^}jbY{Su3Ph??OQKf!ulQ#$9?Q`f-kD@v7m=%m-y|ydPwTeo^VgOV7r=79aGDgcfHi*2&h5eCF-|bpnDO5@Lj}1 zbGmMDtzIh}u-pS9rW^-j7GX3kiHoeI{0Rb<0BzZ=?hfV^NrOb~Tc9}+_Qs96e9hA% z`2fJGz{}#WmbC21F0tFkGHE5xFy@i$d#fre{?i?(7QqS6!3wp{GF+9-Oy(>J0)8w; z^8t^2OY>-JnZ0^9#-8C7ZZS@N>kQpINvUTfV(U+>Dek&n_g0Q1wB`r5|0j*tvdOG= z4E89{o7uGW&>uqscu|@2kcHFoFWq1(>uR91UAocLsJg%naAqs8?&vW0dI}%fL)b(M z%8=XJ+s&SkEb5s0rtGakf?oisLQYXC+K-?>=a1dNQz*V<6tJ-kSxpl zrivy=PTJBO9z7n&Gzwl6n#bT^$^afb>-z9-h_IzR@w#j`PEz3$5A`hF}six_cIl#_hfVW^y! z-hfc=OsqBy0xXeO-GwNT?s31d1cyIn8HmnO-3$*KY*BiAZ1$@~Zr(?ai=YMN$_DX<)U?c_nw?eBo{WW!W+P*#Vxa7v4ux}K=GYtm z#(8gMlH6>CvpP!F(=)sHhQq?BGbyz0A*10S5GaFUo{*o3MY*otc1>u&>ER0DAMdux zrrebw%LYucw68p-@cStZ(|r3>$=SV`yBbeveB#x6`n(pYn*&cCIWg~- zbrJ*z54mkFS3GbneD3xo;UptC_$%8I&*)2;ck~X@Db8S3T8GnY?D1F_4!_MV4orX~N)+~6 zZ7o!Z4)^K@fZf1ZQtRCbp=2A%S8u6VHFPh2^*xUQbUvB*!oTdSpNHJE;*9;KOImI0 z1~%HSts|vb32^+WnysEZ&oJF2AQi4^<j^n-_I+XW#)0GThg4G`Pv| z@{3c7)Q5@s0N$GVf*kl--l>agXiL`oiHJO#nuOJqRvf)7?F*XI=UXD28o4c^W%i z_!A_~!^@?UtB^o=u_rAlnfFL?_?`H+;htgDuG$T}4=CRH_TFuLUX#K5ujk=LcaaaGB5c~v1na=m3PTxWH!&vZdI;|qwh!jiYr(qiwNwY5yNKSTS5^;HQjH$ zrPqq#{#+b-tRd~>I!5wtbzd$ux0_jTe{;sn$6LWgpp9YIM>CazO8*GuvM1VSUpdqu zJi@p)FFr0(CNLL>X-+f0WsY&9Qi1JGltj?ungp2MXBlQ*)ypNQ%*s=S%1ThG30s)dyT$K#WOXpZh;I#LrVEMkN^sEsZSAJ(N3bWhGkco7yAG4nx zs005X5|WEV%vnc_f>#}vq^p<_)o=YDPxJJ@^1ZH2@f=5a4V4sNDiciVHP&XdJ_6#O znuq8!TK`;jdQIQx(Y}_N0D{fOKAqIDnD;JNLv-JhF=Onuz<=7<5G;R&KJnM`#@IAm zg}SFHy{#i@HnaS>XAJO{h`Q{a9aw@&**N*pytdYJ+YHy@JW>aT51y7f@FJ@xOH5pB zQHlu`1@~L^dnoENzg3D!s#b~JAnrhH;kdwlvIpKX`gOfCj*Uv_I*Nd3J6O&2O=SDd zN<;r77p)v^7a8v8)_j;Ka`?Q`6@uHy>1=G*p{DeG=XC-CpX6xd@NFbT) zoj3aic@H&=#-KG`B-Zw*%Ox)4Ft6H}rUNeK;ZGOa|7#+nVtbiM~V<0}kA?md&Q! zMka5W&ZeFCu>)3oc%I-M7KCV}D81U@RepFG-Z@L0QT;$|YFeC0(@ZqYmf3boRa)u% zgNDy#JUr-L2W(}t++hE*m`@5_^KPLIT4(Zh{GKeXkkje)9lUi;&P5jMj*(iqA8SRa zzbQb>X`a9u5-l>i@_|mxkDL2b-)$P1`7R8$NxFtKUvausAZP^0WK6FZ$Y1R5~)R9op^StatGkSMS| z-nTz2>8WFjp6^6)2#Rceo!mq=7m&vb+Pfx+WoIBCJGI(-&(`SakKC>t$xEbsyUDP_2lNX1cm#HMuhE?U6EHm}(4>GE4hlv7Zg-SF- zSlp6qU07sRXJd)iJd-}z2P7Ti5$(7P*4|r$ins$JeL9v;nOkZ8!<@le$c5VMnRq@q zSD_dMP2^aT>a6SHh_Zl-Regw50~A9i~Bn&j~}D#Nl0 z=L9UN49nHj+?=wz<@`A?VV&C_!6w)PlZURPJ(GrWPfm>LU~*li-2)b76qOADE+5SS z6F8-jRnnj*rGu^)Ih-KL(j~i}b&)r{tt!;nR^(x_qyfl+Bleypc?o%ic*`!VmE-tl zj9W8DDR`>iL`EAGl&Y4tFvZvm#4J0hahkg8Bf@2+It&;wl=O8;+v=Nyx+yNOhHJ_wEHwe!$^ckp<0Mb4{&`yg2-Varn!b3fcN=m&tlA z5&YMtMcyZ54wSS`9cj|;33;=o?UbZPY|(!Z*<21^{P|G}j&o8_g(}j{I(t=TXJUyq zlBk;Ayhcnl=nf6;MA4Y`=-MhqQ7ex{YVbp*cDAlacIb%JpER~|*5jD@vX|>y%p>4T75OT zT?$$RwjZy*dD+(?ZhjEemC5YA2zjvx84pG00mhRak2#_C1=->2a>^PhVPgG#ZSgxC z$2`&fT^M;6dyCoWRYYegiJBj$86pn8&|fAcU=HxAi2NQru2OGc$Z6uif+JoQm_6Gz zO{{cQgslc6qbo-yLkr{6L=KC!yu5peGA3tbD;>yVxy+S zP^iuKPmuojp8HS`wSxc`)#~G5(~rzB9s<2b&+89sMZ2`)u`e+0a?U<~5OIKVSY}Ru z8)vsOx2Oj#9BRjmS%0w$(WHAaUS@YsfYAgbLzDKfUO((vEn8w89cKWl_$6C>*7NJ# z_MoS-XQZC%@mtp5g=!IrX&$$e8wRUdi4_>Eoz4n4Or5cUjfodNc)B_vz;1s_EuiEtx#}02yR2IpK0s zRu-^bm>aj)f;-?SJ|iIpesK_?l_Eb-K180 zjr5JX+?!AjmW2t_VBI~w18Jwd(M3B`zjxJXqoWdGib{6EAhcwr&L?|8{#Mk|*BZTYCzW_U*B~ zmk|4vg>vsVBV}%8q7=y6>aWCL6AzNOfB@8q!_zJw#|-)YJ3p}U-+8{6O_}mN^np}LYTc59 zKpmU|(;tL&>WK=QbWMJ-tQ3`c)kLxC6p_y8-L&8BL78B zQXIuPhTk<(n(-K0TJ1kM?lyCsl%9a4bXX8vEJ6rCa_uH4tHux;0;#*}!(2frLEn`@ z;rkLh1$+vs7|kwRh{tY>46sa}Q0jl8-33F;I8{5v@wGN8C>XyD&-l7GR&As~VLjy; z)l^u*#SluE%s0c6v$mYYNegm%KKVy%n~e7#5x`K9%2hhnNO%HF-DrOq8@f0K@0A~d z-T^K6O9F%ho~7;66SBa5_E_n)8z6AbLkGXp1a{h>-f{5W%dsWF2>bV!*qv{=lGmX< zYR+DfwK`JnB`h~!ux1_%&1m<0!C)WXQlZ(vBj&}V!+xXVE2eXrtSakl>|r{w^LYh( zq(Z)jABvNl@8{9B#1L3pP1h8Q^!5W@J+mU3yHUPv|%)L9-7jp;=G8GhEW$Z|N7#>;FMy?>G|1 z3D`XnS$ZHSdv~OFy0D```047#LwjE91R$_f8%q5MX4WKoT!sN0eoyCpI3{u>5XotB zOuyb@*-~ldYFO+u_|b1nOa}5wZ!ZH^JdS+v&3*jSv3Rta*bNFwetXunVa4wyOPSpH zQPSqrDSfZHR}1pmlHEL0Y8gNB(^T%0(w{`328T(%g{gF$3r@d2=k|Bnp49672J07E z%Rn`&bpbx;l;+u)1u8pLx2RzY!usbhc*=20$j$MrpEEqr{p+X?TUOSDq1_O086zF5 z%>mJg2+ikw^ksQ6ws?O$)aT1GN0u@tZ`vagqeb;$;5n)k1GM?9tQK2T6vW0O$4Sm- zRXkpWv-U~$8WbN9f1@eqK$=5xXe7xTmN&bQRpI&)`rUnZ$_EL23dWF3azdBVao z8F7yp$Pu?r`?*-`;(hSaPg{tXl$BIdQjhx01+0TNU*S(E6?!nm{Mh(w20;Y$1D`~T zB%iCP z+Mn7%IC{ziLG=`KoJJpR%;_DrtURN9^WRHX^ZiFE`a#2Q1!+oA!qWnn8#{-yyx1r4 z7>|y@Yg-;s$_XUaXF7WT?(4Y>8bKr$%dwK6vgK`7Qyu4fsBvG`=m0ab&ko$pNxYSN zubLk$te=ZyqyXN6uWo|P#)Xr|5ok-a1u#gM+A8dMG}5q{1r^n`R5s}$7`yMAH8HJG zAYbYO?n2RyBPO>e-i>^i1^v=c3e?A#(|KoB_V5;xWLLByi3!4;BxZi>Z^q21L-!1` zUoCtczdXIqliJnT5K;mSSs4wut3gJL!fmdER>XnqC-xxbcOQfTun1TT42TOeK~=gYy&mTC_OP9SVW_;n?DWd^U3 zyd&BNY!SE?jk0fP7We0n_mr9co9U(a&F9ZrAnT_C{UD!a_ZPOB zN@&~gynQXlrTz9V<6`n{8=l&cAvBNh6SGGTdDG0k%!Vl83F2J3zmX8HGjjlWDOB!m z!_=QSc1e?cX4vD5SARqij>el$Jj>Q?NQW==Inh3lPt2rKo-3NGT_#-){ctR$oyfH< z4iw&ba3VDEH~OK>guG3!?t`C@Xl)a7We?XNt=hK(K(k)uwhOdEQr&(II;BBtVn$VK zbYYUF#;IG;D9Le0y(FE|dqp6#l(qcs@*UWJa5t~r7FiiE3({qpPjcpAj270_IudH6 zm0R}S>LL+SK2#*@#1(!C{E$D0s7Dtr#aqIAt!;O{b9p!;u021N4ykx-vowC3^!i_e z9p!6(%h733wj{c*b!OH#ACdmx=6R_DSTj6^ZEau9{D*g?-y#)77*It@K{KbiFQTS5 zoinfKq0AOEOICZuTX^c}L6|n`$2HF1m`@kiUn)T~={;H?^fye$D7FXtJ#}mJE)WAP z0-lEH*l;V-^-rU7KHdS$*Rye&oh*Sqf~EQm#-(NE?YG~uaoySaJz$5ph*c!|mt4kz zL^e(??SW`DV5h_SR^aUWxoQmzxoc<1tm8ASgh9yFm~;6`nI+@XkU`=L%;&tBA-y;M z0r9;~38^$VU!@)Hv|d4wp~O8GlTk)n`b`V(PmjoH%3psYI*n&VJH(=Xl?%kg>T3b{ghHyM?d!zXtp#Ms8h*bf{r^;2NzYg3{b0KP zSJ6w?maEQb>h;-cl_ftJ7+j3-+fDCApE~T(XI>o?ATG#m4&Gzy`5W(JPlD9e{NpG3W zQyt8_{kQix1zlU-bQ5%8iU=87>M~5e!3*Dr36{Vo2tuRnc}*kFjz>C++nLS`<$nEg zBZLzVNXH_x)wQ;Fx$r4x;5lNhui@tEZzSXF+(HhPV#5G@oBN_ri1!)UAxDzj&$P5; z7U~*WhDPMg6*tNIZO$2@Q@*j++r)T<2%lI~*||TQ2xX~f;dd^7TiJ4vl@7aeAflE2 z!6H^I15Kdwu=m*q#ofSr&&cwrh6?_E)T4<|a{2`=AtopRygg$lE8-|;f+Qw6k)T!;B}L>eX9&=zZJ zp~{z_Dhj3l!nxohqs9J&kmwM8r!lkl{{ty8un!aa69)6=Z_5u9|G;AYzkfWyrVTK8 zL{NE=qXI@N4pqRfgGvztg5EM*8!79CnI`!7(&$f(`&@7|&?~8RMpW{;u-B2qJ12+X z>vjalO`oVHK1O)QYIe^-MQlSaHC%!?|=TK*y+qzE!s^Q-(UVj@pgTWi~DIG5RG=_6BGlz3pX8q|a-{AbDx zrKc+~X0hLBRJh>VT9qU`mJ^&PR&t&E5`h=&)3#!@P^KG#%QhZk)|A(#OYKJ=f(as9 zL_|cy@2;T#f42fibN+-p@*tqNo&7I}%K!T3cUn+f{GV)reU$KE_0F}=mfhCHgSHRK zQHfb4qP>@@l4?Z8{o8x6$+?;cgU3LdsQ9IYMDnX*Na-o7@>s$rD%SFrE70p$c^E1ck9out#=IqZW1U=4e|#t>O&ee+h#Fimz|$w8ozYrIP{oYlgy z;&hvi+$|XKP_BzU@OI!E`GErIP=ufHJ6vQ%zh5UsFM5GqK_`-inG| zSz!2}C%n1ZUq{-_gB5b+Q>hCU5-&q!+_c>!w9=%a#cMH693J1n;fTHx2%N6aIfR(Eir7S@KM?QAZ zoP)M*3=2*fiPTS?`F!+%td7=3^<0e?|uJqOkcyb>T$E?*RZSRjD6T_aW9h*e|f${K{c!MWW4^aKAK0lXV*Zo|jeJSl<%JP0n)(7*Pi~TsfoH(UXLdowQD`??jTW{ zsY)X+fF#J{eJkV13A&BaZA!I9k9&GReuumof->2?*643=pN}+*&j^#}5a)kj3FC2@ ze1>QJkK;*Qv&!ea?k4SdE-z~R^~oLk4I938!u5RtGz2_L9zZXB`p~7ev3+8}GV+o) zY$;J7;nO+QBNyOUJ&R3nFD51v8MtE)9>Ptj)!ga*XTweM`h@>Fml!Wnw`u{IPKoAj zp%ZnltiXlu*D%hmN<_3C0~F8$X`haqy~mf_-qy$3954QS6|I|D278Em&l1m`=gvqb z%lk9m3jgP-`+JFO@yaEXb~G{w#D|MiHY%Qq8iK>lxypAb9K4VpX1#n*ND!YEZf?%! z$+cRG>Ft)6j|a}^O?!o_q`y_)%7#TwlO3M&R;~VSP>0E&=j8(GwvEe8HPz0~%VQ5Z z+)oAobY^J)*DQMZCT6g` ze(nE{znHU87zP!{n@E&DhmAd$A)r#MMmga9@QSqhkY(r}L|zM@HnzVZ8-Z}llJvuQ zL7TxtZ5hxYsnqpdLG&AXj zA=xkh-Su0l2K2oN-#ez1l^Z_#notMS`!=M$HvKEl!1Q(kJUpxvf#s48c$cKd_GhlJ zP&MO4H8&YH_L9|Y>nr8gv^R%1kY^#S$4TS~+lJ2}@>>P`zXfm7(EJ1A{D1d9BQf)Z zl%S%Ew%6YaBrchxPJ^azIl%vKUiP12{<{r3Pc9k0M0Trn+{qR|CW8c{8|xbjl#9$_ zX3viHZ@vvK^$jZ6x{F5JYNk56`q-vFL&Hm~of|KRIoy;i0I#2n1!g%}=`gzS3oQ6{ zUe>eK_*+MyHjy4DzCoXc+-*{m-v1Bw-ZCtXuFV#PK!5~?1lIr|1Shy91cyKpNN{hw zac>$Y!Mhs?u0fOFPU8f3Z`>UkYh0hN=auu$oNu0)`M&F&bIyVmqnuLwrY*MOECe6_vT1pLc;=sJk(XyNet zN?#I-dyDrsebDmQF1e}A?IWnPt#JdfoB3%fYccNaARv8dvcgwM>?zv2cY4=z>q|{u zJ5!Ll38){BXukj#wrX4IUQo1cjz;~PTl!SH^=Y>{=kw?uf6g)1n<$V+vKW7$<- zJp-Ayc5oqy$(2_H2oCZW7!`973HKjMZ8=lC;*ifFemiYr`io}XiHxF5P5~}M zU%iy%i53&N8QwtbSN9|$$_PCyHwCIGQ@k#ZNh2l&X-Clyb=rK3wu{StTeqLLfp(bP zLbYuJN^tEbi|y|&{`hO=;}DFgJz0x(#Z*MFX%xCNV(;2ogDTAqb9~+?7AG}pmiIKe zkj+~8byo4a>C}-`KySa{KpyL1(aadB3bK(o~uvINKRS zUP0r3>ts34saDsBzOpjeu6o$A9A(7ALm8Ae$EVke7&49|o9RB_nzegJS}%LBpCgMf zpyu0O~&G&^mFOhuuaktZU;|Mni8RixH{3AuIPB{cHRqA67#))`BM7 zO^A*@z+uh=xT)VL10FF9a>IzkJJ_RMiPayrJJY?(S1muK<|!0J7wa}6gy&1VP9*W!H}bWDTm#x8lu^yE4>LZQ6x8m;|PP3Y}*jK~YN zCJa4;9X7K%7Ke@c=6;$TC|4WVpxu;_sOkU|+3H9a$;uqUSJkzXf*7AM7jzUOt7Yvn z5k*8Qc&eJr%$5~|d7zkRe}2Z%oj+A-^+Y&+Ta_v9U)7}y$si& zW%U4U<;EVH$&p(!dOFt`yLzn_$;?1D7(qd;R+=$By)P_F%A|B2bZ;2g{Bdg&eer!f zkkbP1=J)skH45sTC@TO;#9H8IH5K;;LnMsfbb4vFxRKHj;Msyqb^rtL#>mOkWgQ>6QJU--Wg9* za2KGi9Fjk6Y?Mf3#g=?Lu&CV$1-U_iw>MQCikS}Xhq$(-Pe(iF2l+E-S-PNZQT~$~ zZd_<3XolLoa@%GVx z8|uZ=g7X17K`olGXD5`(>7cUDx zwUask428QYOW>9rNCl$}5<6z*ruK~BDZTGKYA-0*sxFi%Z%f$i?;}5iJ+~+%_{paA zp3}DPyysH32QWV(YoNcbwCMpWw3@ASk(5d9{ScI($nCcv`no;{Oblk(FO|>T_R#A< zn~I0IYMN0;q;f~cQ4G3FxF;qlG*%7n)uME?dGwlCSQ{*X!~j}yL(cwwozeU%@^uU( z?$0-2Jg0RH^on%5Pm(()cO-l!S63-2UZM4hT2t;JuO!81N42O9*q*LN57NnsF|shY z*J7?z(NBAxAfJAGx~uu2zPHm0{+wKylA`|O>j+{`lCnCrEb-K%CF3_lj#3!qR)bDw z5y+q}iExWRm>UFg)iaNP&u%*5)C7vtN;kdCX4kN#w58LJ>ediEK0dbB%Ue~Cl6>3s zDv3VSS_^TG>tDv3bimoU#-)=VhvTlo5{oz8)T0a0f7glr}c6eIj@$*Lk z`q!1bM!)TEB1c1C+L`Y>?&=(X8F{Uf3Ein@L2}PcFo1wzzr58$GSkv^YQKFWj&e}S zdO43TG^SpH#<{pWPhCS1yFzRq$_^iRI7Dc}mIaDKs^?Ddj2@>`t;1RU=Wu#m@7V@d zp9hQBdt0@88#Z{Jap_CSf2o>#HmS~$qzg6{jg#zlHyF)Nbg0dJ!!CFAGVo!6IyPgq zkM>WoufL^Pes?=m|JXfNwbz1_%kT}CNk7NTLNv77XA!kN+#M!cDK?*rt<;am>k$l^ z;~g&R6WS>7vWL91<(NqM+RojfBiR`U;C>&$t4`a;TA2Y=#6)5vq`oCJb9Ahlq|g~i z>Z(w;G+OvEncMrHJSdyZ%XWYM-R@`V%C&vjtk*qT$sB)DKB*PJBtCSoWYS^zhS*}= zQq)P~NtZ~tBvV`w(;b|VDNb)|<7jqs1k2C+^C$J(u|xmJ;~6S?!gFWe9|mo-IjLbO z=kP4#144&o65QquVp5y7Rx~Y>DsVTKXb^fQ- zgxO-Mh({s5fyaD=IMs`^_84RskKcJdS#^$oUI-12e5sY{tCNN95U?dc8K?F6r|JvE zH{07+PsQUtz|FieNkew;drCXqymK_X>~2`L?tQ95CjHrC$z*;Vfh7(OuZmQn-`Rxa_>=|g$ZZ6BGZEr? zqDEe)j6Zwu?~p?OX!|=oB;&peCGXxUo5A;VAu8r}k<}X;p+bx3Jnjt7c2O_Cg$2(O z0=FQsuVbK2M4t5`n+^208J4eeeQF8J1nP=}8R84HN}=_GpUb)$g^FDDx?SOGaGnDX zaEU<4YWz;-Yru)yel>z_rAT07{A*yONBr0OK zehzm-y*E-3F`AJT@g|)W?241!=EmPbJRg|54ecBz#bv*C8{gQI>bpu<4KH0iSj+jY z5HFz(Xx`onmwH4?o1A|lHEXlw%+lh|P1GZ<#=zLNn0IO)9S*uTOF_mS>6&f_HlI}o+jx9$ElepFxIH> zg$>WHWY>G)p|HDgFc9HcU0Hn^=%Pgb@SOs~S5KqRXWP5&OgPr?DsW>ugWp<@HPG;! zf`>D&eP*DPcCveQGcTs2zO&0vm<}DV_KMm;z{J@jpk7Cw>MWwk*I)SEUp*0xXe_5R zH|?%pU=L*&3g35wj37N3KU|$_+>RMB0^;(lxCc4+G+?jM}Y>jTTz@_FON_Xog zLTn~cEYR@2r(*^4uqnVif!zgG1>&WWChRs|86|9d;>M*Mk%H7N+R)=x<$UIrRZ-^Y z1>s&ag~Ihl`b5gDCv2J$d;H8o%Jc%uo&4n`t<{icPNld@A#=g z*CFK4*qgQJK~^b-Xlz$hD9+v(AvOwvv?H05ZNIdsBa`rI6zx}eAq}i$>am^VX!A_> z{R6`d!(o_Xn)T$N;k889nt({f!Ju|*r>$25CAM>q&gwaaqVopwu;ul$rjKi&?KOQu zJB4SG!M4(oE^1%*)8sZ2>Z~QA8{#_~HyUT9!3R}!l7mtF&9)WnmwmCCaX%i7Ghnb< zL+=_ue_*gu_T9b;TIG~vc7cdF3!)U}cn}d*s-=%dS<c5~D_!wk-r3>p96k z+q0|O1kP;ra@gQ(5d)ehAq`0v#5Pe#V!6Y9{l}@YkuF}>m!dzZ%sd!65WpVlTCp*d zD=u=o_8pcUiXE)EUe`AmQN6q7e2J8@E*9Q_1~XvCRoRp}gVTKJ8{UxM@*{(Ia}~!t zX*W}&x;vqT^l&pfQn4ZH4G`_&MiYn-a~IQ%AcQ=4Z&MUT#rMjQ5N4@dDU3`_pD%Y{ z$10yqJ-c8s=#Zmy*_MOS7TT4( zXTMHph=4Uc8owpiYly1NS*}qcHc!LuGp&}rc9Vu!RQ=IWjfI{FE|M9rnl~VO7;l&!zoK`~DOj(~;e!NS4c8Y92xXA@(G--U}?rre(>hNRte4*dt zBi+1LjALGb5OW8*5@KM0V-h+K{fz#_-f`Zqt5iznF0*8f&W|c~t|reKqah??Sn%FC zS@;#a26L8hcrG@G(akOhSYG;8yzp}Lp zKetiuC$0IQil5?vi4k0aPB$!as(w*1Z~8vMx;#Loe-b^z%)+yO97l3T{Jam-=?oVa z_Tlq@!>f>EcQ3OHM@p$1-RZC4_5%GmLI9tk@E~F6kkdMkgT2ZX19nB3RRLOuI3tGSdbngd5w zIkHD52Nde~;ap~xE%ntrQJJ-j*m=Mj-TG=>YZ=)@ovwnujv?X*JjQ8d+j`kE&$?Vp%###M*u%zkKilui{8$ zD!D2cfUX)yr1S+j>Y97YqkRoL1k!|$|G=nx_jJWom(F0&qivIicTO=Ewd#3W)$N?{ z_Uk=OErAgIbIiPW*_2fV@zS{Q(}oKWYRcYUkn(ko16j8vhw1meWdP~=7GDFqTl?7TdFznJ4o74Lu{@Td z$Qcv!w8g|9M6*-=QdxCJ@dVE`E^s#aE_cewpv;loK^0b&=v^fhzf&1V=8a+WjG zdcm%z$C!J&@4&pe*6_;-V&Or+3!uA1lbR%Q997NdGo-z)dM@xiDa77-Z&+#gcY3YJ5&a|_erZ(`L zn)yHvq^Br++aobH?F1wKv?l$ie)lXD(m6FitMNHVleGl!QmpF*X!zB-C&$Q{r?jj} zxCos&qgy|rp3q(`qp$!?4FB&J%<(0}8xQ9@7gN&*Rz2SPUU3mPlAAjx{dh3~HrY0I zP~ey;=VSwd6rg9t-c`Yd@fp==knkT9_Ag68{Mx|z`&3`q-tiv*E00H0{=gtey?cG|xa5Dp zBEj$egFXI%(eV9qDFf1fIzjRD@6^Ln!~elY;3SihP;`f^ zu{yRCUKP769CcjoTHXyr@y(_@UY;lxb%rXd&Ps8E0>-wi&&6fj;9qxz%X*GB1wv$| zlIc|jle+pp_Zv^}NG^S@w;_8Vc?ZngS}h>*WT8M?km2aWR4=Mu(_P(kWP|!1%a9>gz&E9H zq$_!eWk`cjan^W%GR6aC6sD0^cgT)Lha7)2DVR}$!yFHsJ`sN`)Hcgrlfh=?==L)I zF;ZuKkh6x<>qp_h%sa{3wA%u2c179Ra-F>EHv|wE>GwEXn!{ePk{xB+I+YAz>^uHV{wR8 zqWLT@XW+Q3uSPllr(^fP_5x?R?~){q#;V{$G#j`L`h6bvizN%E z)f9Eb<7yom6)FPRuRYojalndVgihX{hu_mQVP9_nE@7eKgGk`HB6*!n| zDG~%85YVy`n-M&bd-G3(HU2q=`crV^KR>+8-v;L90OHhudu!=u(fln>GXw9)^Oquq z+m^ENMwQ1MJFS)vN5$SX|i2m1og{1s1r$nLg|6`MPkX0zZX$3ETX2Z_7k=OEy z;Q(#QoB8eLgW=36sNKvtBKFPuu%qYO88VKYdp=D>M(P<2hcip5zKP_i3VSXxMnIFJO~WSD2h3zTEmji)O)JZO>5LKOQ35nWE3cmii~WkUL^x zAXwF17{}!-tjFATJbb_|fz6!TbtnZt3?purelnP?-D~$^Mxn+`alt-+GKG4lo{})# zwQ@|d)wi*r4q15l2L|EV-sKwK&k4s*?Iliw7u-wsd%bS7$w)B&^E1UIT-`k7yGpBADeyGR?lu-*`xTRwW~WA6vW#7N{vFg`Ywnc zo$-HMIL>ulv+8l7YOHsqf{P69Aue`NqbFK>x5i)kj{Rs~affK$rN+2fzF6^B*XvT0 zsuaXH2N3 zD}J7DPLxn~*bN`?<@&YPjUt5oVxjCiJ!oVg2 zgMR%P9{ATG&?pC^K^|nZdcDm~`pQ`Q#Mqnb!e6L%MH2AJKrOVhpCuc5)&msVr{yO0 zvSHb$HN+1sGhk01q661ih3(05dC=>%yAXnl#Gi|`l~?`7$~Z8@8~|tP5vh$@HC+AJ zf)tx@(@1D78k}TVAS!bG}GlOWK6WyerDPRYqH4`Uo#@J-GXCylJ7JhaQ!`P zBgIK_zPSmshiD*3Gi?9t6_KC-HKfB^s>eorv8y2K3J;>BRHni{sRA~*P0>RaVC8O$cx#fMs60CP z+E+{362vIZvf*PJTqg9)hRcb9W!C}>n!}qChXl?U!s;X~f-utKC-&8Ezd8TjtZmng zDRsuJbA0itO0dEE8)!Q#Z?GLi4*tEiJKLkesc5s>q^k^I=xVXZ%i4DJwYR=PTWYJt ztk4a<+qixAWV1&%8#8S*995ka;5pNP)sl>9bm%P1$PR6Q6=sTtXA(V^am^FX71}*r z|AG>%BRQ!#-(YJ7l>IzF5BN0KIT9e^x)riLGWmQnJ~QUSh)}>5>7M zXIlGQmJIx~g+^=!6?w!9oDNF6~{&)y>>j>5PzPEAEhS! z`_7tiw?0GyW-TK0yRtv}=*+amuk-SIh=M|w8Z*ntX$`(I62D4A6<`L-Wse&k>nReOWRmkm+nx(u|<^KR31=**{2 zKR-{t6QQ@1PJC~2@;Yl|Y-q@YHMp2Cws3Wk&&i^hAuQYy^;}e%2%1_;CL} z3lRS5YKuQ_Z0Pkb;~n>sg8$v97aHyXfBFMsO~~lS?20mf%;#0=qmoMO*ic#LD~;`8 ziI#rGgEmC-uk~o9%Z%P<;A9p&SA+5FG08CLNn6LU*VNVDb0S{RU>WX9W_DXYzkch; zO+ZO?Oa8Wyzj%zS@5=;ma2?a-#k0s6^9QcGsu)|5vV0fKjQ65g)o+ZGA*_p_%WhT zjaD!ujy&)dSZryS}dN;jOh%nA+#!}D3_$i#BJqE#}p3bb#NFfanJqZ z-J41Ex9sbY^t$mzbN22C-Gp}4CY%#NK}xJ_$sR1LepOs%zxHF@UoBODxRiNKmu@~F zD=dC()ZwZ> z`H9^!nMW4^C!W`7sx7}Nho$`SjMgCLJe`V^;m~$NK-!zp-rfPF(@>66Mf(dGh5xLV z{3Cnnzj^K9-z}^kYX2kVmB~k+pM3LjnxyU!6EtlpN}I zi}1hB|+Gbt~BI*WXg_0w@w}MC*7s?a18lSaA|y%q<#QaAXxcExelR zvwonvw%ZfgtQOGZyN*i{YNsd3D_V3g6xdB>lt>4vRoS%q5Dee3bv6IXVH&?1zj zIElBm1<9v#My9Oe@S#)LHMC^D)Uo5?Vr`!rHtV}|Wb23Ux zq1x4K5Z|eF3MZNM>!3BO9lCb@_P(0|LIqboqu!<3vfZ$&+bEu5nh0$EKw9gxai&La ztpelR(N+@SA)(8|Q*xW;G7?9M5A2@85m6e)#0>W4VMrc1JZqkyyGC%bXkbazQ!IaZ zT`&<#Db%=l(1T73*z6MDw=J6~S0DJCf4{5y%vG2DHW$LUT`xjnl~o(PW}V;DZ5b6k zCA3N&)QfKsS>=1G4!>=lOzfQL;|=EE0x$w-JqE}Wi^3BW`AdoD%KR@XXltUE7qGrp z(Jc}X>Dl~L7;rChs1t^}@jTG*9WO!qefXBQ&v*$WICs<9UvQF7M2H%!j=Ub6_BA%T zttx@{pYLrI^htzsszs%8&?hm441YX`5>04`_JqY0=t(vY?Xj>()~I`wh={2)UAZ!d zlsJ|{Qs6}E&(yY2am|KE$9_{>v0z&74gocz_+(R9QWG+K2q>noP=RYtv^7oHFFH4A z+ip3@$v2hSsL@NF())UY)%p(%d7rg+XF#fVmHd<`)BBdW=;W!}5G^-7{C=M+g9E@) z#aTB+c0>F4bY*lAue6R3Jzc0CegI$4efViMH_gdZGL6jO=>IG{{hP@2bY%aU=?{!I zjoFV{%S#6Q`%=meUT=JO_UH^S@88!f7FTC80AfV__=ufO?MHsE@z-UJ9GRQN<@+m@ zzb+vp%4n~^*BcyvS-upMVOKEz@(0GZg1>(J82zv8{KE7bs0+Q*k5SyQDPlgw6a3Xb z3#b2=P1*iDca}SZksnNOwce!;92B%<;lv;l$_)Y;ZjR6CGh}Nuk=VTwD@t(iP>Y8P z{iMRPiJ9BLMs9*V?^`s>M93Oi_vtLBeuTk;5o8iUaIpL#c7!KL<=P#z+u2hUn4KJ0hBd!6&cPCz=0dL z?A-avXwu$1)6EY7=`Wk75Q;^)3Ry+lg5gfZTEU9^L|A$Uxr-+ zLy~!+wb5MP{=j(tHRp|-?!<1lC8WUnT>|FB7W;@3YD2mkEY+;U_wj#z#QuA9<^LKa zrg-OO-_v@zJ9N&lG8h%DNkfH%d}OjQjSp;F)SF(M{CzHSX4&Aqaob)SlYuv5wDgYe zm7;Ik65>WO`eTg43ExDy3|z&Jqa>k!Exv|fDp5jazO}k|Dl;J=2Ge^TZP?s*@CF2` zp7R?l8jvEM70#s1x8ql&&5wPH57!y3%#s|QE*35ZY|8^eGi`Nt1ZqL`Y`=Bq&m|kH zBqmyEq|)zxaNt$CJ|Pl54BZWvfr{xJ36-7W62kc04%xm0PuTgu2p}ckVG{%-wt^m3 zKRth;Sw=NlE84M)aigBF7GhFyYpLTJIoYsW8e#Z3mfHxcU|NIVQE_E$&{_RDwM@W0N)d6(GKIUM^#r41C>u0d%Ziy64_!470Tuj7Is{W zKxpXQ)Q7ctcNaRTcxZ_F*RB;S$mI8u3$bHu0y|<{d#u5jI#jIi``S7e3ofC83^DT% zYIAcYaYgU~=cv?x zvyBb6xHwbL>lkd+l3rr~7djH^)B;A@?naOqqMX|aHE+U7Y5 zsq$Nnd=758ma^yZJbjR^SfH^k5J~&7mn$~f0-T{YGGgGFg%fc9h8Hi$)n#;ZAsO~+ zj`?ooVDn1i3X=X5e}1bDz78LfU`>zJ?s%iAT`A--G;Q4X9F}|Ey5E3nY!!|AkWDZP zSB(dRN2~Sqq+L|!eQ$TfEWriTF=5Ccn_BqYQ9Y>3JJI@wK@>PHjy~V(M-G?DP7XJ# zw}YkN;K7Xf!L)F0InyK*OgU7a!LYIM_X&~4JQpM?rZFmfB&1%r&P8m#gGAA93*9TO zxL9Q>;4x&TJ8M~Fc-i1Q5C}K<0K?-PGpF<_0+G$c>#HmNjKR|l4!;ta^F~a4`QL#@ z>;IY&k6ZJ3ZkWi%ojzWpluOqEFSfx=V$^&vmQ7fra>j19k^>Ft2iOgz52f!rvsMuj zV$M}eWHSx_UbLWfbQ|!ARwZAKFLDCH4aS5Y;npGjvu8IPo$hWbX57R+T!cU#v0zoc zIcGF0&L(Iqo-%jQvTuTzCU49`Xb2hdVP%95!@`31==;ta-fSN#_7pFXZBfVXR8$cM zG2|w&0n_M=1)+2EN?0{jqjNpJg$jSwL;opBh(95qOi+|SSuduuyxCMBpX-Uu_|fR& z!ynEFo!1?1cpVO_#V& z_PQ0?=0(%jZzuyp@)y$_eyV&)i=Kybj*SeP3P14a!9NVWS&GFY zeZO*lv7W(r=%>Ua|N38=y!@N4m;#^NqnnifWm^!o<|Vp`{r`Ub|K6GZJ)Pik8xO^l z^_6lAPn<>G=U)W_H(#|i`yD`Lq`l0pxIb~;{+A%KzvB72A6~^boW(~}j{F}e1Hjg- zOl~;)8c~UO@E4+r;1o?j>Q37ZmfM{@^C?#t+u~21IrDBFBQeRIX@8$>X%}5#lPVfm z431Q|Ic)AO1J+;mn8Ij>S(x^y&8-gcFQ!Fg8k-t>Ph^Va;VBS*0t-m!=&vbByK@ks zU*E zrR`5&Kz}0_(WgHAM&CJPb0u@ODAkcHL7c((Inl?#md#DrZuR5dh`vuDA>zg1c;Um9 zPrvuQ=W-F|qd!Mi>y=%Q36*}*-X4!~_;zNQ#9xMko>zw2%-yIGTfkfiqw~9@JFPIb z!tN+>N!HfKFj*PPwu|Ryb^}hjcPg6SHb%eI1GkeKjGlY$&&{gb#P(_L46Zm#0J(I` zGcRqSb`Wc4$swMR>GKDk`*?y0W$?$t;jvR}V=cL@-9q=7^&sW(=Jo~OMBEr?2T989 z*gGXy{&^#rWLFy#>E2RevHjuv_f?K*i|L2wvt;56;)m7*C!4PHS%%Hmj*hQ|c9oAx zYCj_A)s=Jazn6`R|P#an&2j<$q!(~+ZkL6upX3H}FP9DwaNdzC3luVEIu zL*&dla^@vU@pmr<9D(f%NV?p8oe-76f==zgfpnxrlEsnG{74&%AYJPl9z^f?`D?Q? z1@CD|hnKFLMZX7v7nQ@RNXM-ui#@^--IXooPklmoHl>Q(e+28GWDTaO*8l_dxdDom zgXSN)5Pp11Et1Er7JOAQc+o)^5+3sy^fzGe4TY8qQoTzcm%_SdT;x+lv6TW&YL>DS z{mm7If68%Ca{RJ@_J9PvV^TORi^2A~mA$W4dZu&-uos)SBlL5up6g{cw{Vr2FzLjm z4O0O4u%;@ndxbFaX;$(uRGB9N3I-Wr#$&pZV(48(2N+XtAl)$38+5ruhK#Q&B!|tb*=R=)70A%g zSQ<#+2;RJr0Rjcpz~zA@&oH2A3m+XYYD{>>APJ<}ohufpm>TR^fdg|A%#Fl~O{ipt zmb*DxeauMpIKRG9IQMA`(4RhWhu9{tq(qTV{b1tu)s{9}9$S*T(;%*jDURRENC$bj zoTV%gTIsIlbs1Tsyj(+*rdhGhpJqNRayv4Uvc`d`p)=N+H@G#+Sa$Q!jVTe0n~lgSo`c6OP+&qo5~h7td+e%!`Mt zQByc?^L`$E0wtsezPoDM$O7l^8ge^S| zgXWGz{%9$$^=&lJqRrf_=xRwPY`UxOP%RfF`T*@zY}oDVVMH6+y!(6(m=VgZ>C>K? zxVW(-!aG;YU5JWTHv-jnXCtjbalc*ohZ1ZcnpSR`fZQ`FL#6adR(S*i`Aq!vh3O)E zu(&sOMwe z?{;-dgE+$%$MeIwdL&r)3>z;>_88C6fH$43m+w332??;Qtki)$3U${oR!-0?73*cG z6QQ#JgVq{(8EoQ~-!9A*qtd+t&$s|_keTO#<{UMmyKY#GEcHZl@F;U&YND0Te8W;9 zB3NZ!>csu*p(_FaTBsEi6j|I{;%R{1Ou4i0>9S9!;2@-wQW)-9Ejxj?8+y`%|He-GQs^;tXdq#jpp`>&b^63MGh@aXqiRGbN*W><`6NFlf%t6 zaH@1YQ2W7+d`$Nwxp)o|Cwh!Q549i|yfHVh*K^%a#NkZ2T1#l9{oE#Gjljvv>pb*< z3_Yy0V&OyY#~tkmpaSy^k4mW%0LHn%*~4Ji6~?(hmq%xlqghIyFCak}>r5U=V_|KL zLGlPI(W>l3T*j~mfLxnzv*jCQ+61I5pV#?9ZJ2&CcPysptVJ|rTrmsNVA8MA&Nbe* z&kb5SKkV5k)~M2YsWKh8D6d0LOJi87U!^RWGa+hS57+7sDvAc4V!_100*9vkE3u%Y>hA{EUNkO37@;(2`wux zTjP9FKi$>E+%xMnEtdl!{qQn~ZiJ&bxSm!>T8?FMfN7JXdVsdcTI$F9SI(Z5@- zSb!PJbdo8LoEa8dGx|n_(H0>xK?9PI9J8WWdHiHry8A3_5q*|1hT`kno0sP=+qXA4 z2Ql`N@=zx1KSZ+87U#6LA3()XaEoYhJi23v`JPk7Ts=(BQw-?6>U{a|e+5#p>nFv?$4Wsc-9RZ4;YbTm!bNu#HoXSe?Xx?)!RBVxMoxwP69t35)g*@VS zS1W1!kV>zMb`H{?w0fR`haFga(FKXUln*XcsjitxkPzs(;D-X;Hp?D}1zf!U0|T0%&i?AD zBwcV?EwkbJXQW?YKYqhqTIiO{PxKX;zcC4DRNtzRUTb;raH)szDx**HOAo$U3FnE* z_CWDE*nnRP}t&k0P>qT9t3D z(k|9UEXz^SQm0KSZEVqr2xfmc@j(@cX zXo?GFntLLcK)z0gIro0rt>%LTI*R+eaVN{&QvmtGxQNr#@dpM^2k1si9n7g`3&+yx zWYDj`b^HEnv$w>-QLVd-1<5n}_!k#^Z_+Aa$Iz}xL{R?6ym;%OxnsYvW3{}CXm%y5 z`xeDBn^OxyZle3px__#wpjCGd^Dp2w4!Y(juYJru zESzB}7^E08%HVH%ITt5PPBFbnN$Kre-9cU)tu&3orzbJ@nxm@_`MKTG)wS|{AxDof zHhipU0PA+5C48{fdUe+E$Ch5d~?u_}6)@pqC% zO5vjGyjesHW;5-o9`;h-lWRU&dLz6@k`Pw~g~B~~iT$r8Ax60|D4a(OTR0-|jom&u z)6$(Ae*!@M9oc^pt;P+~=LQ)+DMq2)eiRD_UW{VIDzhRp1^!#v028jWfWKT~uoRjO zpime72FF(m!mK5&@C}|Vk{j5injKXcEEW#~JX_LT+3Jv>B;VK5CuwfX7sCnD7cglS zUHD!=93LsU8w;UO5Y9nS(^Rdx(z1o?;v1ag2d!eTUWdAy^g1nyH=TbhPKvqEx2d)a z@pkyyZe@Txh4C>uavVr-tA90onj%eFk5%DIykc|4H8*Lr0%MFOJQBD`+$rlsYa zmt>#lRP^kM8HQvs_n4t}2F%u%L`wCS3i}lYAL>#JKML%%qUY<<+6{A`0O#7FjS(>n zR&j=ZI9iwk>NG{7%WWT5u?kGa+l$=iOa z*0(mhTKk1=0im*8XD|Ja`jPbDp&_${bJW3@#0?4HAGGP;@QXc6tE_)C{-3kGHUhUy@qye{Lt5pT$q%HKEDx%S5@8Z zEhS14Y}PHFeBlw?PQ<(AOI=tm+9CUsSGxO@{aC7RGgIlCB#ssi&|uW@gp=^rqQmie zrf}9L$;${GM888)4iRpWt+Dp91h3)FND znEGth7r$_6_4c`Tj)JtZ(m;sM5K@s-!)7vU9ynN@9}XSak#| zhb}FIbVd8-Zuw>XPvNYnm?1C&zM{tiJv`jh>8JFlUKSSfSgC<1k}h~hNRa1u}o%XJ?FEKEwtP4ozUtxdNrod4TQE;hCpi5i!XcaHZ_DSl7*g#QfzT z3&0iPETM<4M^xnq#%iuX-?XdtWI9FSrG(0g==Eon(1gPLgeqt;*8I^jmJ%`dDL8sb z1oSk2&F-7${wNe*5ZS3w_O;}v?nuXy@({PRrwgX@!J8AQcxMu~M<>&TKF%GQs&3t| z-f>@@eX0!EM?f)U2Y7W2uoox_HIUzSIsB>@FE^q{1`}|g>qr#~za3v(4c5H6= zsv$?qtjZwk8E>*6%YWB?qqg+n4~$pecR-$c6LNnOMg40u?7v@8Wt7t0NQOXjVfek+ z^rYD(1^Q}Tr_Y{6{MSZ@3v7Yu+5sP<* z(u!x_1=iKeKQPFSLP?x@8Ec&>cKEy1Vig=j7)`w9l3A6MZ;;Zz z-~4|S9{k&T|8M^lYMpC80mWAxTEX8OQ#?_@=MS4#30_gtvsWAlrBzL<%GL9-_;x$o ze0Gob+dJ^`^QdmZ)5n`C5b~LJf``3AoGu$v;R+P_!D*lN(f>Rk_V{VqVsM` z_7T);TUybVwJToa@wvmk*Kx^~S~vaM3cT0r$BW0Pk$)I#UxY{cOK=`EuPV zRb^p%&F2)ssk*un+WtuHnJh+at10G8~7>{6FQLdpr~BAIB$Rj%ex^*}~Rbs6eDxY*SK#0J;!7ik_a$=F*cSpGjyVn$u>11 zPsFI@uw(j4pID3;JEB{gG`jS`OE1Gu@WWPP!PCpDGk0ul}~q#rh-mZ|Q0c zl_3G6h9Zw3EjVSmjWtwcnUNA=k7?)XJ%bD7fm%{*NcM~_;B`N3E~jo^QNF@_70tEO zY!ZR=dRQ-m9X{uVOnazxxLirSqJeomYm72vGET>7oeeTmJgBZZUcWof-*Vu_BnnZtEFJ{9R)^rZ0pBP39$oTbY&hcVl;nsMxW$ z@E)~8Z``&b>*PP-=}B!tWEE~S>}nlC$M#dy4JiwEB`u`?IB`&9yjXNM zZ-S$xdnGUWv`|x|_xO;w5C52>(XyVP*JUtfp$;PA=f)x;P_;6VNfozkxJuF<$(-a= z@91ovtd}`X{A3vzvsZACcgZg}XL*}@cJ)V~^Qb6JAZpO7e0X_jorcti{*4$O@jTwBr= zSj9-KWWBe8XV0>*btc;W-F&@4D5I@8v!=!kVjw){*gn|Sv_XjZnL%Wv1)}Sa5f@sX z)y*i!1%Zv#afaja$amb}?>GpWZYC3(o%VEEn%65s$fA zpWkwEhUAO|OZRir-S`%;aom6@i!3l+V^=R#HwbW3(hTgwDiYgbz%-o$c5Eu|4r zXK;Y~NP+T-!=#M{@_;vEc2w`4HNqpGVVcqL{WpJTeEn;R)s2Rct7Da%XGLiqa|(+r zh9PJ|w5O)B1sSqn3>=Y}w{$kJ25-K)rh^1eL)XCl@zPq8OU$5JC2>s(&wr@5Z^|0wTpByP;cJO6`~! zGUr`nrd)QoZlvv<&o5Az)K1*t`eECKaaEzLu_MvodM$2gWF`AU-ZQQTuSF2mBDAz+u(ZgGXsds ztJ-+Wd!2P%dyRG6se9c~vjx6;`C%t*g5y8-zSJ9LJzn&{C3wDnFdzgwBbqbzdUKGp6Z7Hm_ zi;BbpkvR2RxsTS!k<&Z^i{&KxqM$2mihM}3f2Ed&B=%QX4Y`3xSx+z1FWVhGqAZ1X zQ5ii9OghXmUzudBRI}5&#Jqf?*)c1`#)XZyih?~Go9}GqCh~M%_=aL%(!ig=rXyU< zI1!D?x_Y+O9cC?ar`UVd?;`c!-f)#3ec*|Xru2!)oH2V@wC2M1TRwUaJW;s_+qzx*uHFs4sO&I_`UR{mMJUS!vA%aSnUrB+a7js^G#wgL^s8+$qd%x){hDh1f z*T$)dk3`Cil^3*65RHJr6py8F1g6#fX1;RpnhSnx2DcoJ7riEh3rOJ?_;dx|;Jaj! z!yqg)-tj099&0q!(Z$0JEzY0bz}#!vuYk;LLtK`!+zk&YBHSKv77Iya4j(wYJE#;K zStbZDE+ZIh`Nr;{8@K5ahF}8W&K2~ET>UyN8*Lb~Bk;6VP$>{fhXWa?4^4hKWL z6Ky*7d#8rPshQkuCdb<*L4f)!>j3!7YNu=el6L8;KVzPboaUQ4CmQF!t<`Zo%(&3<<|K zPcIMoi*L?z^!eudV0jKwk-{2s{(YVDt0TiRyj!H@ak#^-^;shUG7?D?SedAQM_+!3 zvwm{&;qjyIps(wd$_b(L(_$dM!sb&-@5|&RqqHPW)!V0650^0gecO`+$?boH%*nmy;(?oaE*{dFsrsXU?2HedaO`@3~(EE(;0@ zT;}J$A}T3AeCr)#nK6U|c>^Rpi7fzhKci z@C&M#Te^KJq}+J^`ILa<{h&Xpzip_gSGB7?(6@392&|z8zfPSabNar*S)}7!oHhEN ziR+hR$4{K(lwH5TnHFd0V<-O4^1qLJ@$LzwXOM4m^rI=jIZoT-7q~6}^Z@L%|JldC zb9d~;zmxf&n{~Vl04QD34d6xX(cIh(ch>8EP0BQNdM?kawD0owXh$ z4bwpla85`zXvg)*E-r<1fj3%c9_n*2Igm+#Gf=2K_l&KrImOF=e$FA?{l^+|d5cW6 z8G)`5t!wgE^757~^NWjTOHCamY^N@Pvn62F1QW*v`vPu6<$YjTmzsWmex#tW0&pu_9ie2qD(evctY zS8b4mjmuw-lGP!PN3_W0q!?l=WUP)G&Ba_swWd~(!-YR@0GEI(+B2qQ**TdNQu{J* z6e=YP7G028uytc6x0GRW)Xhzi0luLIqB`F@klAL2izOv%<;hlwK;U9Q>Xb)OM$Dqq zRh>{VnQ@)_Tkqm@u-1p z{bnTzaRsVefyUqb{@SEj`U<<+QI)dF-XL;)(;T3ky^iMk93G>W{Qb#~g&S3i?enHV zPL|JSkd9e?*pZC-g!GIcCprA{Lor56o!BqV_)W8P`6oMsCYe;piN%|JG6Nu-S8aq> z1lQy@7HvpmTrPxpY#ACh7HRN!aWo`o6ILM6I)NWZZ8bc&!hXGPw4lMRND{z}w$sGJ%akeZvnu9db&mmkk@G>UUo7KH<)U zI=GJdsh~p7>53;`8OCmKn79Ihzi(V)VsnW8>v;lJ8;LT7}%edxe)K zhSL05FpJ*+fU}%Gukyl}yEu*^WWUXt2ChrA2_%1Q#Kni#)e4Q|rem@p5ffrryb8;@ zsDZig3OzBU~YP4okwnA0l=q-@xMFiw6jWERYNmRK`i@LsPX|yc*YX>&f3mR4F zgZEK%`8x7icwCT}hD;RwQTiS~H)o%=M+{=8;Uetv-Xh1^O55O)^$)3LXi2=U%T9<`pGE>jJx0_5 zI~J6c8j7`h*LzxmRXw}wy?WAQgZt{g&_aNPZDQ=e*F)d_SlrLcrRnDM;+JIiN~^?7 z1Sf1La@Q=W-l-uj;(?z(&dg-5IZnBnDh>%Kx$_P*K_FqbX0+x|j@D9A4{qiJ57e0l z=}b%;8kxs~r^agQKW^W4*W}G=%OT76boY~04iwTL-4v0L8z~@*H`zgbYQu2|re1D( z{C_67Kl2xW>)KYrcNeH2a1!`F7|+*(YhVY&L43-?=!BIP<`f}Tb+*rkqVt&~NYLn? zAq}L?*}6oDTRUb17Az$_k1tzIF<&A1Zq?2$8&)8j0}r*nlX5g=i!{xUsf(BMp096i zx4GwK?`{m(|1w+;L*gPIJor9!h^6dbc90(0b5IhqeF{!~KF>VcD2EAqxHIY2oEn&( zzio{mu#A8FwxU9W|4yHzj(Uus&`Bf`0$ek-FOpKJ<7txGZK7+N<7--&djBX0C{tvMsU(?4!~hq7$-H%PYhzb|F!i^^G-z z`qOhE7Kh@q0j#aYFsMk;Nvj6Gr0yp+`WIT?5}PF(>RQ@DYx;$Ck=Lo=i^5e0r=7M}b z;qGT_`^Bf#Dyz)Q0uw?uHHhhYiuk9b%*kZ~{t11>Dp{qCSyI&j%zZ7y&<26G?Hv1JpAkoOthurjL(!U%lUu8@$fjA zUkJ$XN{uL*-y#k4(7$i-^9pCay2%hpW5GpT%D+<3RXp*wcZMJ%mei>N&PDPvD>5OJ zi!Wv?1WTqqGZo2@zx7Cj1{M6ET*s4jD+4wHLj@#_XS_39hN;f~1dgDtl%iv&5mnIe_~#jYM*x5RtGxzg8_VBt_<11PCZQ-=RJ+&D zgdnx`$&LU;z$JImMOFJp`ziDNE#62l1Rdz0e=jywa;mV1JVo;Uyp@+gV;(2|>P{Ve z^%{|xp$8HcYjp75B6(lW_A|zB(G*l~bl z$NYKrF#r$%)u^FQSi8FHwzFSs(#gR>fn6$mQGa zQcyIHOPx*eVAcPn_?rea?3}$fQQLTRwYxVU;n!6^Oqb3OBRxHZ^j`DjzhWGAoM$Kl z`BqWjEu(sM>x)Hn)HK@3F5q$OkO?z25hxJ5VI|4APj{tS_HkO2)O+bsTx_Y#T zH`d!yK6-TW5mDNnyi+Ko=zav~`Ad6gsQtP`>iZ)=veUY{#SwrFKLRiu z{k8Quuug|aa$sG~yXruc1M3l~1*E^hx=2*m5n#gi^ATWjQjaAg7LfOlrRHQrzNehQ z*8>#{ORZfsy*FOUyOMh_PML`igDzr*1IBSWzz;ZTn-pP%Ld5SSzkLvMo9o!_vLL9-a=YZ~Jh(x(GPhv9&IY_45-J zr1K1*V2h7^?wyh?XYar4{NEsjL7v5`3t%$>sd`Y%ikRpm@b!Ly61`I8Q(B(2 zRsy5$ooB|hrlcgZV!$s!dM`|;!`N>8|> zwwB^={|Feze*gfuL+K;H&b@&nK-9M*z+RJy86F4OYbg@$94HL%lsr87c=kR?^-YbjOQ(dagKWtSc?yxs%~SqO(G&VQ_>TizQN z-*y>KI}jUR(G6|oiYKqzJarDY{$eUR&Bz$dNt{%aKj=4MPe+7mHni#q#I+|%op(&v zc9qc=?YDy>5DWrrb&naL2(UW53g6)^*4uFd9Rbc5d|=pHE5Nv)Q0GfUS#a(cMBL5E zoBP&Su^yRny{OJGHmNpu0zgN9?D>u=r8k}{g)nbTvUq((R_fw$!mIleeW6w7TWOM% zO$7Y9xIy%61N*Pey<0(1ETUIav$E@hXAg)X(%u(J_5{)BS>6^2j#CQLD8Kwleo>*~HTDWq)?Pok@x_apw}HUh5W4*Azd;9pbYSE><}oKcfEo2~fr zKbizbCFKC#)X$ROY2di!rBsx9;bjZ~euH~L8P5DV8?>~aN#a_i9|4Yqbin;eCkYkU zaF&UQ+IZCCy38(IQEEO=r&xMrnE6rnD`!SR`o<8CidvPee>>i>f2Nne6xcd_p59N| zJ9hKmR{DRE!#%%vWlpBw=yogt5pEb&#`^=V8)U$?hy;n^9pb)i1Tz{&4Y6cY+E0C- zNOJRyDVAf~UmOaqnY63L_BaAO;H7MJg>KIu``RlCDKv>ZtWDgCFtA1NA<=G@3c4mpHzAB_U(XhG)GJ!u8oB#W6{EhYC zqizJZ9*<=R+ec+KY%i+Lh4XYy*E8Za@@6NCbrp+RV!ey@=UlBHcKK&brNC$vANZmS zlD?y}@TKVqd=RUl17fY<_~jPW9aYBofSqo*0ukS2@U}j6ZEuonCC8CjG)KQm08Q5NSIus!64CYov<_BwxYcry%m06&X&jrYw&`7GHfFjndeDgsX2MSs3|))4bn$-Piq{|Gv<&aYBa>-BdV{W9sB;0KjC!PBnq zqs>`A?k4!cKG5ZSCrx|wZ=1m`OHAx;MmWM;^<;ECTO*1CLfOXR0*V4L-+St;hRULc zW1AdGo2PiqH`l3Q2O;((Q%K}=Euq^?1ANM;*44(e7-&3m{#y}wdxa$qp|KS%;Nz7f zPEntC@Jw`>8xEh46ugm}Vgz-(!#V;y6}30D=qGP5sC)uZw`)5in?L8ak%KOf2OSku zfm;SuSszQ^1kZd=~CT6l!;9b??);oA zQxWreG~G6E_z#e|*&FMb;mt)m^s9IXG#ru88A0)H6gX#zDK#%?mbQnV>W9)9Gf5x( zF}7z4-|^&sOT3eZEbLmAVQZ~#9ki_n!#eJMqXS!{g&q0Nxs=91Qx_FzlNvfWC|pC0 z;C+VUdEjgPW6AOea43X00<yZZ=EL=SO9I-cGH(L70ObZQdTn=JFPVn(|6%hy)8KKJeQ5*d&W&?4{d#pWiIiA zdZ$Kqp|TnEfVc9eBK8ItS))oiJfKKBoi^hs>0*vmN6YE_<1GR$sQ}w z@wmW<`GX+TL{FwPB?LgV30>9y4em9-9TzZicDdCmLju3-NKPZi_&f=oilX{0#XJU1i69e%j#xA24>8LY_nZexNk+%gG&%VX;;jiOSiAxG%o=sKPcvd36G)wTgTw;2( z?db^*eOxNs@)>mq>mM8$a~cvg>6$3DkLkK)=z3RPzq{wqW==6^x~R?VxyTVf4u5z$ zN_x(fF}{gZ7ml)ov=lvXDo*CL3Xzsxup1F0<(2PvT-``Sjr|{Gmbl@n|$jCP~|-SZel}c+%=(ah84lw zfko277JqE^(2@##`PU183L5Wyd_VFL2wNqoPwipon0-67|;!u=xr7$lA<8&2gu z0<3!-0b2h$zbiCOTr+qQ#Sa++-|t!y6J1!8G$)qrg}8BazLMsQ%B2>PB6cIvl^COx zX)4Ai7tllt?u^Q-Jc#i;E6po0kAr^fENR<-c?hgC~vX4lseuoW9 z(^RG;$x+U>?Cf@iu8!?u6#a5v4hJSV24ZapK@J+d6n6)swHz~|TRz6G&QPs>bBB@W zXhBoS2cmuL4J1Y)qeR}gPHIy1e$AMa@v2kY@DX$RacGd^K7_H?yV_)tL-ipq-tvuj z8w)Lx(a~SkxiPilv}~}djJeRWaZ`60nfqtzvbT{zHCcA-nke@0PXA}J=aOerc-VXUz6Oqa z^qk{lh-Te*@ODLLTV6LCGQZkGTa*4Np1Ks`rX#2ZK9}ti=NRu!+xG-BYELl_tvxF> z)>OX08HE&RgMCvqM0QphRPm^}6q&j}^3HfCNlYK_Q)O@s1Dy&IlNUDkSPF8;t{(in z7W>KveTU)4=Fo7jc84m)Hb&=yULnsrd&XQF&Qy;PS3R_KK=Ola6G-Uk^{)E0Fo7T9 zr-k(xu*GU%q-LAq{C%~{^ZGTb0b9hU`HDe%lAG{%;`*CT4@aVYp??rHapZsaKJ{4! z_8Xkwn!FU|spL68XzQcz9b22W9c6LX_q$p5A(9b4BL|$2`P~hdmgC2Z1z&;qn){g6)C(0&L05e| z!aRZoib4~c?$TDAUItW@4i@iA}Cmyx9I>`G2k1`@J{nfZl{OuDG+4|`Kb1H&K~ zU)v5$OFgOhzWPbgVi{AJrL%TZyZcp4R2$9z-!RAji;R?F5_L3|*a?I*97EM8qRqvQol5ST9*8vr!G&jt2Qn)v2x4 zWw{Dv9zXJLe4ftuV%+F85JMsKM07`8hE52$k0bE@N|8~O6&kF+%|hQP+l5`R&_k2= z1$%gPi-n$V-@mhfSmG%b93 zCA`O^;LkJUk`p*ev(sCU>CB;zq+)5gAMDOaps*b*Q6qvGy8)>R#g{gp>Ax4>?E;@% zE5Ymn`zp@g88$aI2^&yY3+Muq(eL`rYBi~fCB#c>;dG2tYRewi zDc%SB8Z=W01kJZ#ky+LvV~~KpP{Bhi9y(aOk-9)RtxF0_+0AIpK)iL*XSMEmmg31P zz0{IY3E_(xyrWy*<&#T&s$bfbK#lc`@F~X08^TYbL>L(iq^{J-5P=+!Tx4bCrcy<~ zC-7zDCuzboKLSL+MSwLSuy_rT4|BFWUZ^oOhrII+>q-rg21!4~K9k^SE6tG=<Xx(+$;@zDdOhh4d@8GYHK3XI8-5PUehmz$`o{;4QHyECCPm{8)b{F1!A$I@s4?USD)T(Sze2dR8$-x zA*9BI+b7>yC3okpLR(v@{4vA$0T zRu^40a1Sz6ph@*7SQ{A)pqztH1ONp|7u+v+ZQrrskiK zP;oxYa8((}yEXE6^f>Y4e~BfWNSNb7uFmbIDy?67B2af?*`IJZf*a^M@~WRtRNN6@ z!A(+9u+gyaavfpkH=kA&fy1J_nQQb9ZL4?KbisUvvUzN(9mLC{!tEr1q6DAvI3$Y; z#j%G$>lb1HKCpjP|KrK=|ALifS~y-%pAajKT_DRlwOwl1MCXnd&_0v;wD`#`u>K^Q z#IEBG`wtZLGl>C3KrvX*0CFyLMn9I9JE1vE2bcQ?K_?MqyeocmQDWzG#?sTiL*=0h|Z^Pt)H3e zzPrH+ald|0?Y!~Vn8krNefyBK=tryp*xY|bmh1W2T@m{=(LmfOKgJs}TbSV393dA@ zl&H93XSWfwKz4Q~gX40NOifiw>u|n*Af;Nd0&}0Ao&cQ~juZ!#!ms&roW@HW89nsF zM)ANwg!v>ra|olCgEL6!n{8-Gyr*kz(vXJx&UUgbXT`nemX_;!3f;5lp#D`^T`9d_ zIX^0Vm9%4cC*axE?%CIANr>q0kO?MmTT`a4?8UtxYeH1AhmU~|Mp4Z61%N^hMQgq7 zc!JbFDRoj7k%Hp?FnYc%gm5{%Uz`C#!EY&m83KGvGh>V^OA=NQvOhgF!!ck(e;#w{ z^KWs|h^=fyYw^Cvf;?MFoV_g$%u~YMN#ft>#1N6cfUI+m>@R2Sh0Dat4e#z3d-Y5K z3g?u19_|N|(Ui^E7C*5iHj{uE%)<=Mq_%`;r zKBcUElQ(7iYD?Ys6f80>rKdQq?sNFbx^#OlXI~QbGz6TTE)P{@HW>PmaE#vo1n6N% zkIPmYXwEr6q(-GuSAvY9UvfCyG!=`e+iE2_^GQ*qWz^O^-T`VbyQL9UD@$1xM27SAG z|8<3MO2#Afb$g4(FSk%K4$IA{Aq3{WL(U9IC)W|MHPy5j{j~(Go`iazC-AP_ojw8% zyo#=4rOEf3nU6fpHa4tFVb;0rwfZGF9K&rL0j^e+Hud5f&w*dOaTl^Zv&emV&DhK{ zVv0ataeaBklsl)3vVXq>So$fjO5=BD{Y=gq?g#JUId4cI7&7nhZO@$70e@u}Uy>r; za^%mIP(~3>_eE+IVY%yf>iM@Z@;^8K;x69W1#hiCa@(3w=W~)nbk0JdRAqE`NrQvS zwC9;$|HzItR18s+XVtie=Azm=XfT0q^;SIKRB& z-TEa>&rn0hqVt{^JWaaW>}+~&4%$=2@GuDkT4i{>SkBFxr8l-Dre~O0B;zc6;E0MQ zr4DRbB_x$yR_`3^R}ZnHV}3Heau+?{#-7)rX$*nXR`*#Xku5A{kn-{yfb_961*c{aww0aPT^21x}v+J z><#^5N?g3F-X&khsf}>n!T8IX!Q*G?w+EfwTcG-|CgjP1{8cS&>j8a?% zGZ1nk2TQ5VUsn*7J`JP%!>k0dcOWD|YBM3_2-Jpi`P)a(9St( zDPEFdu?w8Bb z)4VaS)3B3b_fx8JH3%CY7*%I3mrC61krpi0yBO|MSJQH^PL4VqlWgh`#>$h)mfI-A zcHviE=UDa>joAG`m~ygn82EZw)ON&vP4U8rpvF>sWXazCW~J}9DoE<{l~sQPECS)U zY`8vAi(i@nz0Q-fdHj5?f9bZ~P3JNVLm7oNtbKEUN8sKK+%xI`4B}Xh=NJ;Ge1E%ny_+rz4D;TuJq=@p??f5Kfy7jL3ubk-4l%*E z{)L&Vxx!NL6?vd??d9f|Cbj;ii{@sj-cW*_udYoi`=NnWb)kCOX+L7{^4Q`)0e#Q1-_98DJiEMT5 z@xD5R?hu@(uM(=CJmTC+^Bd^7lKZ|6zYluTbRg|B_He_%lJ0KnsIj-M%oqyEDg+7#M-zmJW7h6LZkr3HfqM$i`V?s})$-u8L*-CM9? zj#tv@aGkAxKZk;zuk?DW0MkGD(7J1BwQ$eJZ>e8*_%-Q6TgPO<*M4}is5n!L%;(So z!#s2e6Hb%Wdu<*3#f!QUd?-|G@Nw^jszSHLq-CMlxMpe=I-J1zc@w{rc}pz1zv5(8 zz+e7QPGGT&uI8kyS3*F3<+p)hg0a_rtDPT=4YG*0e4c+f)qKc!@>n0PL$+U0StKaA zRL!#gj;(6#SrI2MuN9C%ULZ+QOhC4?&vpbDnE+IqhAF`Acjqe(8}n|!MPLqkbH ztWRMPwf>`w9*!Mw_F^;B+f-9dE1^yA^y7tTD7w}iVogdiMMViX?j#!B!#dvA{T5X0tu{|CeO(?& zbluLYPdYcuz^Iz~e6XP`EX=w+xBOWl&HvaU2;-}49{+iG6-4L&+i9%R&IMPxM)yEJ zXxwp*6z(qiiNkx&Y^wwDem=F5McLs`A47A3^3DR^QAS^3o!#baQp>11zqjLWuzr*! zjlTKZ3MDmdgv;)zXpjGHf%{RpN)7X@lui%<|4dkdwAQlgBV9&vSvYz1O}CT={U*vO z`Fk=C6)UgmzWf((dtUt!`xVFSUJL&wgfJP_`)GRvSasU?4Sc~B78kOlH*54iq6S`G z{HJyu(Avr9Za0@rD7BuMrVA=kp}Vyu+T})San*%*=bJjAbn)C>j%s>5Wd$#;_6eM3q`l>l=YwFU=y6X| z)yA70`7@#^Fw;oi2rcf{l)O@tplLUo(wxk*@@O7)Lw(-bavyanMp8Vp=m;Q9cLetY zJFbM4>P>r%ScTi|ux3blgiYqJXk@~Z8nfsg1~3-oK33u%wbavihjO#wpp6&o^g@)4 zwqo7##a8nR1r(a9fa}tv>T6K~HxHA(#nB>j!Mty1E0MO(Y}4>I+Hs0aYC3QOp%CNc zdYOoI6GfjFM99Qt2+KzhSd0~m9er%FN|TR3hT8Ke+yMSL?%D@QZSq+2T7KARupG+R z%BXu;+_%V-EuLqdk@=n>+%+k?_NEyFw!9c@q!^yqLc6nD5%{osn^*opqj$Wgzk3Xp zI-WGCOVzTZfa&rykI5aKifV9hW{yfi8ti_UZ4jAiVvBfpy3;C*AjJ0!XCBk;e1!J zmeC%yQ>j|D72>-b#o-w!`#YgKD3K?qVXe&j%wnm?uIQ5q0(Y3PtP`x0;_` z&nSo^;+fuPm{IGo3A|CbH1!ctWfn0Kv}lOJMC7B?bG!2|mH42Wz~s^Zl*bbMhyFnM z;woG|RH~PT^e$xxurI{0v~VFBBHKz?YCm^QhY?&!m}I7)LF zeMHW8%bzM-kS|KdWPyx5UNK+wKy5L1(-KaR?{o86E=mh&Nt1Y16*cYPdb-+P3sDU1 znCneR!toT!yDN)Hx8iQKDkF?k;saB>A~<5#^5n!*dd=K|>-?yk2*b+L)5Ig#pwM=f`~bN;d#ywfP7zKU6sTR3TuviU6~WXbvW&87z_85bc$-}^L7YFII3=!iso9P%K>`+w%$Zu))#*|6njl9XV zC~DxAa&_AH;^Kg))49AispMtrhbaE+B&cD*fn{ymd!Y$;D0Ch;wYU5QTQuXO9QVw{ z-5u^(%OoDQqPJ=gV=eMpCq_h)^2N-C-!Vn0EOqy>?bIkIgJ@YLJoFo zwz#{eXlnCTA9{l07zz#vk!+|X6!&;k_+fiCn%sq4iX*L^zLh1#K5?LLSz)H9N!ior z^>V^=IqT(@`9@!T_NC$~g^EkF{^<{ImxssVMFiebg%m~^^hLS>R&{iDZqa@8Z6y-O zp1@_xRRy~kSDJKPO!{*##W;d+Ab_~<#}((!hkI&-$B5}$_m5?*#Z`u_(hmufV(y@J zR`WV`V4!_3m-Oe`I}|?ObOa#|5^m@#uXE!>h{T9(;HDMj+uUU380{jzaKrN#;V18He~Pa{aZSX5OU2!3bS>9xsiWm{sKV%n>YX)IL4 zGN&c>bMT;0KfeJ8oD@uE>0te+)QC%rL$hQoW32+5Sof#P=mYvv0-{j{rvyzMAAM~K zUiF%cVBX4jL@ARQVK8PMG18s!zGA4d6eGLU)g42xdOL}3nvr5o@!}$uzxM+be?|F^ z*R_Vm%~6;sU#+oiD*MW*vZZrhWKbc2S7 zRgnHg+b}P&C<0yF6gtHUx$N&uS92Q6fbLS7WKp7K<^iD5bkbIq<5V03yt$p1bgFux zp{dc{bE%F{oWl{i&_#x(jTLTnSvx*qAmSMX(3b!j4v+1~zY=WIWAWOln3&;BizgPM z7*J+pvU@vJD1C8}PDDaxtZ#W$+9o2Oxkyi=iU=V8lrd6w302byS3-${In+6Hk7gtX zuEruGo)M-63ff8q$|G2{vlue)17x_T$yP`x%vM2xunt*Ei5;>2oMITZkQqomPPrOy0CVt7P0cRE$ zo8kR=Bs;X1GFG!6bf6jj%zGiMDH@S?hlosf~o>k~= zu3}U}7f;xfzNa*N)A>>;*r})s9wYUhVOt>PSNrEH8$LWQ+3<)j57`Z|m3fG-JFizt zak5><@5p*CZr*P4dbzcE+iDC_>mZr(H~D;0{KftLyu7mA#rYRu3VEgK$}4}mxjk=* z9ksoHMVdFA4F`GDuxcA<%L@VMG%UL1#q&jXXW74&zh$EzPmkV@DlzG#F=5Uq&i0>a zQ3!e8y-?DhR@3ok9xu&Tk)jDT>i>|$nQq9!Z6656mY>EbHyau4sFTU&T#4ppJ+T3_)O zbGGS^ejDj{$9TC?m4v-XMFq3GKmlL&KpES>U9ugk@;=ekHZwG1-!VEdMw})$bKn%{ z*s_ir-duxr6jm1JCsbTgQNPlA#!)B43b@-}U)}|d18rJtAe^qs`fI?PlNUlyx3laZ zP!C+%$i8{hOiprATS#lx_~bTbr*<@YL?odWLQEJTE6C~Y4clLvUbZfpLYqen6UK&& z!MowNlr;SWctl(vQ00=yh&+_S&-$CDM&@to1!}fhu@oY!lfxP#W3S-wxl%glrQ3_Z zeJhi7+&-pH9wRm7r1<{wRuH#FkaiTkG|_62Y8zt?$z4u$$U?R-?a)qEUi~dh`k=z6 zzL-lIE)HGJZo}EyTJrU6!x?@#?KKAlQ{$r~!s0g{gJW@~dg70~G1#+GVOI5$^%~*X zAdX;>a-frZ{!sNnS%@>0N^vR6r0OpbMLx-3>kMTcqQ_cCNaPd0G_HYDuyfwu+62UU zpux{b(~k~(R-+SC`G?IswN_yv6}s9x6bAEl&O}Spow8oE`TjMzLXCT}7oe$AelWa4 zabZ2<*0rI%(b=w8#-_73l$tFPa}(D+HEhC6%pJ*l+v-xcwQy%C3(d;%4TJWf_o-t0 zWhuj}Q)4w#!=MhX?Bsc-u2vk{c8vaH(!6bG+dCkrU(?4(wW4hPusNc2ysG&uvMsG1 zwUUa_V4JsHSvNHLwf0_i;DIYu`U^(S4LpF7kgc4WuO9PJ!~LW--m?xd(y6k8_!~Rk z`JBQOf1;xr_m*DBIi8av6znErdxuztloSdH-5JTISLhK>s6Ps*Bh$5I=ZkXXSuPcO z@9GwpY!*CC+VreeVPy>nI#F2iaxO=1i4BxvlJM?x!}HFlNCJJhAr(pfqg6R(U&1 z|56tU9MiYxsp>{uA!nu9+4wm)ssFodHNp2{7pL2Zpv%L{U;Z5vqX>KLJkwNTS4<8>tnm>Or6eZJ2~F=qY9At zGiG-W3k{g197nhwqir9%^@EFS@b4xeA|*c#AE!ZC=2$i9YQYcmV1<+xCVhhc~z9)trMAAq3$yy8~>q-Oo= z^#=gBl2WOmYuBSgWxjimmij+CIN41Z($NqIV`%PGh2ECM9p zByY`QS>DWJ{zpK32~w?jn}0Y9X1BwY+k0yptGzg$`D>?_m&A{bt@N3U45vC)1bevc zlg2upD3LQZ=o>6q)rku<1ZqhfzF6L`GBJczCPkjy9L481cD_OHCH0i*U7DTF8V@O4 z@*?$P>KUp!=R2og>w3w~D2O~#siOEifdsXgDit;O?E7GSzG1G)?`P}IC>Ajt?~C0| zoA{7UI@PBxaPwP_bWw|s5G%2@y}iUskyn?XME}{1&s7!O9zHA4y0BZzw@9aN4&MWZav zzQzPUr>lC<@HIh^b@jN+cZgt~<3+GmNgmIVMLR@J1&rWl2OffHdsi)J;uFb?vLed` zSN*K~WJIvMZh=y1`nC+_;7&HZz7X@`6OQKlDH^%`M2+qzoY95iht>tGf6`C0FG@)) z+#K?W%sm_&%o`%sX*WKCT_g%Uhkgip?x0<4Vj6R1I*M+ns)KbDPD)!l(Pd*ppJ0wX zGZMOp-+qBE`6O8XzMK%hyonOONC+QyW~qO^9VRN?&35`mrB^9g5zSG;$BS_eRai4f z{YX@^MG9gJVO*!@rT#BaoJZI1ed(Wb)y!LOrh%b5E+&e|X9Aqq`d@-YB1z&liXHCs z#`7fsN-NlGritKZ|b3D#p(92>K_DR-@^9EweAuc5jYq9mrw9Y*{M}b`o^TyJe zL?WeO=4>iiBInOWLEGn@y>)8fQ|+EgwMI%wqj@<|RtvoNje&mS*Ug`rh6HnLzE5W8 z%TEtsw?!Z0|A2|Oqqd!rTz4vX35)2k6$KR4I#@tFCoavq_IaDK#s$dG<7i02x zA;(CM(DC=|EY8UdRJlqxs(`T(=7mbm=``ir-;79I%8JSvSQz(CXL~4Su5c$E0p^DX z!!YtgVPV8t6>LA%{jAREawERGy8Tp!UlYjb#(3`zhiu97W=@{jUNTi8)dvi20piFv z4>%ss?=j!ab~!xc-Nl#J_hNPT+vwNmm!r1f^k`4QY(SPASbe{qHd@sgl)D2J4|D!WHXL=p|&MYyceJZ;WH29s>11Vy( z`@SY&J41d4mo(urPg&g9NMH4DUhoS`*x}vrML);cpyQ>AUmB3$+t-3&%C08)gU~VdSVr%%ed5{vP-7LLg~GNNYwXDQ()!b&<9V09C@}&&x}vR%39yH!Yie>7 z&0*|;`H;cxXY($Egrtll--k{#+34idxx-?ekD7OFk_VpJWGB3Hd}u=oHs~O@DKC92oTO>cil$Cfw;I*O0pK<`{|J}s*^grki$TXEpr2e#IGzeyn zplTgWlfiBfu75d z41L;V}p)Are2cF~BoaL0s-%`8gc3Z{u-K!3(9vk0VKn1Hocxlm6kriktwHa*a z>7Te;0SJG2oA3GTCNji&f$-2aRLx1Q(z{AH9;rkT+go4pH1?uvJi}tS-~Nngr;yDM zQKJOuo2o*6s}RI#j(Zl8Vs0bMy&?7OFbLIreUAKUJ>Aw1Jl+|D2e2bo@U+zd=bCD#^v^y z2cPG3e=cFXNF76tG@no%z_D1WzeC-&K&P|2=B?_B{<*3;4B}-cXq;VEoUw=W%2=FZ zLhi#4%%N^<{B?%n-ewmOyuR8foV?xx_ z@a|aKrFH2Plo)=_#e2Q@?oL5#Tf}OTUtaACo*&l6nV)rr(rijH!lN;zb=Y5ssgdh& zYe$-N1-FAn5aB&7HY2=5JeY`kySOoo$F0bihYkmg4}Uab9zZxw{uf@Y8l-gkyvyPQByC^ z!fcnb`Zb#pKg>QSAxz#^5ku!qFF^L+du_Jhal1ocn9fTK_qxBPV2O3?KrkrY+653a*%(_aZc55IQVKAO!_MZZz zjrBPVR!;jI%h|rz$w{nMJDW&iy8Wcr+>BPS65D7e)?ZsVaMz*J`BK0<()4O0kI?q2 zJWM)xzY>3~w!3iJCDU;=KHZXJwMVzKf$(j;PMnEtPmd=p!PbP^wicHx>wCgj1w%*i zOnf19S5J-FL98#c&&kWbu-(2y>#5m<=ukeN`|M;-8$#LSCV)v%4(hwxD?O38S3}(N z%`(4(q>VjnKl3TIGR&rmlke_DiA$c>nmpT38`&IhV4Jp4Z z%Dc0V^XicYQGU?=?UbOl?pJD7?qvFtTg#bOa(N2ZbFwIY>xhXRv<$kG&PApL9j z5(#j(T@p6kJ3MucpWb;UpJ^zfFS$zY?o={oM|Rho;U0+mDNtQ0;8dG7a7+7FRG8n$|V3+g8vCXe5K|AyDRODD|fRgoeKz zl&pjWzYVf7FH<|PfUsA5v8NIfr7B5tfF}Rik$P98sank{Zog+uz{yBiX6RRsd1bYA z|77N5=FZeu!U1uM4nobes=UTu>v~XHS=3QupL_+B?8MJZK=|u(T-uMPz2<$4^yeIi z_~nv%QUCh-$SB@)8IovPD=rJZ8NJg!V)w!Fp5G`%+sK_uAA%qcQonwGbA_l$8BI@? zHypb{(J(B69t@$YXYtQFir;)Gtr=X|niVQ;)T$m$Hz!tC(^2h(CyC|PYUdH#^B&-! zOjURW26ts}Bgthr?Yf}wd;fH3lU-{8!TLrjX|0AOKPyk5&#wtM^hbYmCMb-Mbzr4m z>qDmht?8hws%2LrgTi9Geu%@_*SAIot<@~)o7h;i*H>o&;Ee!BdEEGV-HN}DeOCI8 zrKPzX?nRQTl=_X;rf(KaydUJhV~X9HLbjLrg`@bvwXXmHC+u4Fv@2!I*aUx;;g7Xt z?~H|!XbAb{mIndl!i?bJa`l_#;5zdff+t0M@R*^m_xZ1G^RL_@|J=WR5S$N!c`T$9?6%Dv_ySXU^(j-WEG@G7ZD_Z7wJ%8T6Fc zb9Bi%Q-2ERY&vDFptm%!2{{7;^C~^m-g$Ug&^(Na@k{EmjfFzGm6fH6!r4Q6&q!T$ zrM{7{E~2pQeb?1Ws$$1QOF#b4!Sy`jhM$W+{J2@+Br~y$bSjatBpvx5t+*w2_vZ;6 zH}e#Zd#LL-#PeEddZ|_-X1*ce9pU4IUrLm zU(BSeAbrQ`Tv>Unm*42^x<4}WfsKm!Yjji>*OH97gWt7QnI+U&zW;iY^9dT{ueFf> zy;%*iK7}vr-uIp4p}sqh5bV=Wn`iTr#_66}{;NspnO77H*CW#tcV^kE=X@dP7+Na{ zAezctYb&-80YmP}e{bF>H=m95%RjL0Y3T9QvZpoGIS@_L59V>mdEQk*4doMOt+cNT zAG>U1Y%P-nHh+U-zrLouq+tx~Tb2Vb-U&*6Llu+hW14MM`ih~EuJS^E~<;hE)O*7k4EGM!ti5pk+@8n~@d zys{5RKA$dT;bshbjM%3Y>8{-oN&L_{mkhMlk<~vvr!P?u+@cu*nmQ)E0hCEaY7yQ` zrwXYrCDty3aAsa1f})oN0Y#6_Eitt zSSsatPwPA!&>71#gi33^RL~_u{uIcq^0{y;*#)*(z@PfG0j0~96T=d}9sAumC-gf@ z@5QAncf{$kSn;f0fWtb`!rQQ2zKnp@YjFF!o&i&2lQ>V*cHoWnx9Z&Y?!s|>mL)rdA09(cd4X}B^Oe#gHgC7iB ze5W|_LyOWn(dY?rYlq@l_K7DD;pXhcD+dhhIDq=bI&6ho=}}^?V4~9#!(!uaj3B?p z2>Uq5OnaE@s6osL+z3$8U&P4TPkUq7#4V%@ypk{4NbL|>2@kYnC%u*KJ})Xda)_7c z<2IN*7<#?d)*K`6aOj{tURq$EEE#27X7r7UBvtR^{W?62ezymB(Y2)N5Hf|)6qN5w z?w|;p)fM&E+u5dKA~nzTI(dLQxLz~SiE_Qi+pc>7A-Jz-C4 zWeszZqU9!*j_@{CX?FLz$ICVYq_hMk2Z(SpA#xg3>>7}`Z%Sp=U~&F*4G+2r)L|W* z`1X}Ujgp%yZ6A?20pK^e97P#{sy?W1^h3BXycWaP*lUL&U$L+E9cdq3a~GAHdvZnN zNZ5o1Kb?`Q7LERI^qtZS%gnvQBmXYs#)rLT;s;MhN%cILA8B^)cC|_i-(l+O#68A@ z;cqd<_}Tf);AGtU>X5B<7bAsS1fq{nrJ#`1(?a-pqLV|`X~tg}3wr5U6tUUp$Ig2z z{ZdGCS-Or@yVg|o>Q8|$Q=yVxzXdWB%geZDy@7S*ZUgO|{>p0R#3qoE9;~d2LOcGj zWTd^kzM#IykG%BYDGzGz6(p{WRI{!hBtH_JU9&Nd5b`<`@4?->yy)}FptRj~Wqg*t zdO{qEYx^s9 z)Ym=e=8F8$U;69$V8qIqQ!#Udg?cu%r_(8nbCoPMzCJ(YTTWuGGIH2Enlk3ds4cjK ztCtQ9G;)~zo2XEn_5BsRNbUAccZkWhMRjsM(8`c)g8ZHLdjtw4eB?YP<@P3x#(jS! zpxWK(g`_H(S-e___bA7?gV+7!r)vr+($;)emChD)sL^3mlA>fHl=vc5R0hexr=%E$ zb{i$nnjK513Z`xJ=!~M!@^?29NyzCvhLBISz3~Qw= zJ05$48Ef){fa~ zOGj?Or#B4lyvcpHwDzH`f5loQBc?OR(aA9wNLO%x6Za4oQ*U`qmJGc_3#F^=3FT$3 z3f6QmS?JR#FT&IW)tHa=M;SKAdUp@@p8{rLO3;uMDBbVbiU3D2V|k`<77?VM-u+6% z&1PWKETpKk=p!Ou@yGWiw{lTimaYGFImPypS@o`u z-5>OFA3M}e6o#Xn4@ix-8cwYK!d2(}B>9vVcXi8^|1aVFXJYz70RE)(&rk1lsNfvN?bYN-d&0R=lUiuK+?LBT0hPtGxC++BNlRi6LRhJ0R9O%qs zO^w)le@hz7`p>MHF9DBi8dU@J>;oN%XW9q-$>PZze#H^&zMq$ zQTA^QgDf-xZ9oVcPZ<9s+t!V}ZF*Xd=ffZ?gNR2+qq}acE+awDT^LzA@@2-^zw^FE zEWi|og$j1>PPrbyy=iL{03e|)dZ<_}Sa7_v9K@y~6GKs5dLNerg57 zsmFn)@h4tke(k@k20w(U`7E20IL>5;{ZF(w%Mk18xfG~njY&D9vu{i4X0zFgwyt=t zUk~5l!@y!6n@<=jmYIad-*>-SNciE}{0V6jt$(A3+#|+4p7jhhvCxcj@y5>%OyM;*hAotrGU4{k1=$7bLjfs=B4rUKF!uWs-Rg`)-w~DmfO1!t4 zc!b?lgK`co@ZHa+B>oi0I8+Q0VKLzi9ooVZlw>ugQkQLKjVVdx zDp>sN8?k1EvpnIoVW;5C>?H2fvgNZC+{&L7r{Dc4;7t&JZ*Rv4E^?G7sogr<pijsJh)`l89O_FfM~k5TO4>VQ zv(}+~T4$00>LanwOfQW?ew%1ys9h{&KCNUOgh?>(B-uj3n0=FDzt8sV8#6WCheJRq zIg;uTB^nivJRAbZiU1cGC$i1H?%^b7HIVCOIzIJsQvqh&*W&t0@&;KHoZP<9ax?ky z!jW-_niZjFGJVISr!K2sB@r)^*qvFafsaFatk`|DeUY6rI<$e^m>fMu*c4}fY_C$* zvM7!HR%svYY#-|YQoKI@affnNBhvdicJwo0DKX_RuK&psn(Qe4YoP9wE^TwA;5{v> z-&{w@ij>bawfsi=*5T@A9o(`k*=IE$i0Hxy8@#p=$;>I^jrq`_&NAw_Q<%>y%=^fNLwR&u#MEnVFhJlW+4 zU<0F>!y0!FFEg@!`q;jIgIU4VJ)QxjrJj-MLfN%`4jub81WnExTL`nz&!p&CmF+$) zSOVmV8CddVL6S|@bqYNm@Jn;E@6IFy?Y^-tqRO$@;|KSXCPAQys;ZKke3nWBTK4pP zcfWFnR)-OV!pUROLZk6HIGEu5lsq3KAAQ&BylZArAGm@btwedUJ(V5{F{J>g|ciN72z0}WbT?xVT&>=VjhK$T~g2yMJjYC5R2Bz-c(2F~}OUK6bdpKj$ zz=JMRE79=yV65dmmz_vPlIL(O(LT<=)wd^&@q_b{!{1o0?&=djzx>(VLBGcE3C{8T zIh)H8vN94n*F>0R8WzPvA@_HR%wZcS5k}0{vkqH*Bx7wSMug=-fE zEO+i%oK=S~AQpC<^)<(koq@^zIkJ2k(rV8&+fAJNNG12Qc*Sb0zfh}9h4}%A;wK%a z+Vj-i;hp|Ug2;BEH9s1l=3mJ;0J$6wRu%4@FV)}ezOmN!(K)QU2!sV?K28bBsZ2($ zDMcQBW-VpE>-@T>I%Cmy=N$`ZP&a#*TeTXG7ypBv5NI3!_+w_qHXY_$wr{&s+Jq+* zJZ0!|(iFN5TP_KGZ|`SVkEkgD!lK`7!r7-LXcqFz^Mb5?2WaalXC zd@0-w#75bJP&y+{woEspaeJ(*YC?|CJ5T5kxmo)FxjvyyD$CnCt%f#}bpK4l1B|gf z-95dX{nWmuas9HaOYtRf!&mN$`ncFaB#ws&MS8NL=`63VAu6}Q+H-v6^7THWZ>@96 zRyATprVyv#8u=nMih|Rg4aQnZgzjj?c`!2X!Ei9Y{hkV|Gp9i(8r2tYJ6S=CD!WTe z`y-y`S^GE#r$jrmsr45)WV&U~ zeWww6B&>f9%*ncFY6wh*Bm1>_AJ5NOo@R2zE{1fZg+qxKF~R8^h8oSy7|tlz0|8O; zPuybs40H9RFiXp+h9#7Up<(kJC*%oh z!9GD4?C7aW?l{-xUfZFRSDngN3G~PGJ3&<0UCsR;S-#${4EWcvfOM;wxBn>+*O9%2 zvdMnwgLg$CE7?Jomxp326rtq3B#h4#vgp=!%_pUO>>q)M7y8#DraEuct`?0vu@RG) z=`n`s`HX!O!27_^XBLHYWs^Kl$xp zRzQvq4!Swjpta~iAN|ZX{QOi)iQmn6-ODppJBExQwO00xaIl4AFIZOO`XcwIl5fZH z)(WT2yjhIs)A`EosuW=|Ad4##Dhgs9_X3ivZECjJQB4Pv0?PYt&s&zVPo0Zl3dda* zk$TIMpH?8!{SN7Vl*uQWs6-V?afCElqG%s;&RFkeIa=t6e!L2%CSbjQuV~4FF=+g9 z0cg8mK_OrlIGwBxO6;O`JV;CW+@y5~wnQXk#BuA>*AlZcK*0SM-kIDiuin(PhW@ZL&)GP1JlFWnc_LQjEb=8HIS$AHqnI^PaS~eF6xJM9j zR|QS?VV!*AgXVSJz|VD4=a{aQPchZ1Q!WVlaB4bzNEIlEnXA%^jGz5kA~5_|Z#A-F zHgRz1qs6_r$w*Kvqjzs}^VTotg&C$JP5JYCJ=R6bcMWDt&8NT769?nb+O<1F*Z&~R z!j7+R3EX_ZpO+c<8DtxB=OZFN-K>&E8H@Vg4j*xwn$N?*A}y?_Ce*xRGrj$r%v6X0 zPOYc(yuTHE_mE!+#>#2Gyz|IJ06tMR3gnKL^|)I-*FI*jd`n=`=27I_I7srSS$eWiEP zQtR-#K1Z`3|wXTQX6Z|3ufJ0swmal>w>{L+B$fx9C06`BDHxqcdffvNS_ zIBT_nbnl!-r6Juc9Y<)cEI<>WxBj}jhnAj2>aQ`;$xE_Ol1nm|UxobhjNu;w-2b(! z{8`{^-^baNg<9SnV))L82duM;&{IS-&X8+D-x}-HZt{G8N?|!92im!-er^m}!Ll@a(4oEUb#@!GoCgE%A1NJ0A zH}4nDtA)*XHn~AMKGtJr=8p5CYRY_T1iV(h%?MuIt1$ri?lNrVR7uzAsuiR3m-Q?K zc{#?bbQI(9b-vkO_R=lHgP|b20#nu#H{mfoohZdk&YQ}CLTS*0xIex_^(D?Kn1q;# zOuF-36`Pda0lRJBdL$GHo5>mU7Yq1f! z6}fYqMVZ^dFxkr^TL<|{VWag)@l8b1;zXoO_|cs#j&dlY)A{b6($~&Q!reNGC14-$ zG}};qYRqRN&E3u3*3T&af%A|(nK>Z*{n{R0oppgW5o%6S1GJrvGMq>|cdQacc9`GZ zM;%ooz)gP&7)KrVw^ht-7@ZWeLzpEG=U`R* z%CEz7c^QmX{2b8^7wsEY$YDC(p|2e-v`;iIc03)s1pn-XNxQ8 zXnH!MJOdiC)~FyPEq>gR19;V6R~nyqN28%9c4IA9`U-R3GRV?z&2B2k$4#zbmm9?j z5gxI@#Oubd9cGf^JXG(+mmDYbd+&tOT4sizZ4GR>RiM91lim~pB_peH?RfEQp)K4; z320n8$f;pQMsB>nQhTI@P3s&IbkeU2nMG=yt&5a*Jdj8xP9|etqRhil206}AnrT^2 zz!MEA+6<&3fv8)q6*>mAB-*a~+v0*I8C<=R<*Vf&S;20W64^l#@!`9&q-6cP`HYR+ zO_Z4w67K^IV^RbJqr=Z@Lp%|3v6`07#Oc5!;=V@@PnFyPme2QP(#+!2kMKf*lqgG zbah1fA`SG+QQaXsek7Ffy#hEWz}|dsZ5!HGvpB_CAZG*w5%P6!7rNi(+XSupzWQO^}ACC5jdq`qR$0_D7s~F2f(DrB)&#Q3JfJUZ?1+v+8X$2aE2d-R!nv`wR;e zKQ_CXh&89E$s`9=gHx1e6L1^LL4>&@t#9NzQMAZO%bGHNq=7vd>uPFq!uk?(5$pM- zd>Q}5NL5}`%y}kvVyfQ7Z)p$1w_04E+Yi#0c-;~WeB0KIjC$Z~5rUwkXJ@)34P!G8 z_iRuz=lGK@zAbkuBQPkC^TDR=Hr;2R*vd}Kh~(CY78xE~qz~^7J3y(eeFgig#X`~! z?c+zL%m+EG|8_!=~mJ_L+xcC|JVf;s64le{AijW+}>a*Z-PS znNrrR=eIk#v32;-p?^#=Qnl;z{LMwt?^{Zg!K36x6Emwa64y^_^UD=l?$03)h#*13 z8IICN9ya=XjOh0(AykSGi={Pd7)81j=p0VorU@AdKb%IlIote^Z@A=tGkfbJ@3;g7 zN#~UJsk|e+E1tMrQb^2@avXwNgwV{FmTK5?+ZirT*(V8!bE~a$+#3+k~S6dwU4hkHJ012z?y6Sx-?AC)-HhYM+}^ZmNEQmyjo0)`4PYj{2trKlEjd6W!1< zei34$bE+;S$^Oq1u$a&(MrgA3aWSW=E;~2Ty}3$4M!8M{kYdYI#%xX3OkB_9?MUez zO>WKtgq~if#LjCJ##fTX^V6(26zLFV=orYnOqZ-ZWDVM97>ffVj54aQ4@h2&7r2Z& z6@WL*j<$3|f8{22S!kyXnS!ofBNX0LD-jQNdKX4`y(ufcvX=CmJ?I^==I>&;fqvAC z9npCCtvnlWbKaoP9sJw0Psol5)wrJ>>4u@M|AJiHDy<9*;1qYY71XKRvrD|b{hY zfm=YLcm+cs&@WGawUtIoJ99ekdlcsfSL7ytZB%k0)xsiYhCrjln#N2b@0MpL%kwd3 zZ*=6WLX=3Yi4)R&)26;<@5s+D()YsmD#j`;e3!HM{XT(6(wZEu7vJ7&|o!iidNSrCEORr{%7k{1^`GnwFB2tE9XlKN&uA5QWQGh1`33;*OUc zbLgF(ZznC;yRx{vNsC}je(-UQF*=h2hg=@(@5C+|K>d-?S{BfPM4szPo$LH6`;F&~R#o z%f@k}3-?{$-ncUsXKJK=cSJCxA+c@Q(bTXHU_(Yj&5^{@U7GhpRN>(WuF53wW(Th< z8q71L$^UJ|%~|`HMCXN)NVA9b!&R@q6bbPUZ}_tC2}KjKB9s0-AZNca@-U?AMBy13 z(d^CTWY!yO?A#!`>E=GQtRNF)g?2w4`Xr!9gc}g=SFJN;UsiltO|hu|prFq>&8v|c zjLFKMgLL-k*pEGKT5`}=LJMt-j7Uwm6Q!sT2*!>ac3k|IC!Y{$)12&bkvnxjj7a>0 zUsXJdKLEr0!A%mlto$-)z+kjaZ+}^TJ_k1 zu{h%}SoDv6x8Z>Bh)Y`W0L?b3 zR3I!EJe|gMluj9}WBYWi=7f}$aCFMMxK1xHIKKl16^y3tth0A_Pxf3H;YkapX){}N^r5sw>3S#rHQ5oH!bN=U z$`tN-RtUs0S7E3%s|hFd!~^bmwMEc&eK24hKFTCWqbp8SoOTlGX?<50&h6~#?25iu zPy8l_{{d!~n8B$&G}PKO>zz6_bY7PlNLo%9=*YHszV*SkhW_17BT0^k1tG>ozV>bSZ_1eY1Td_4Oy5}PWO2kDXVi@ zOOy=jhW6DX_a1^yDJ6^~y87$cYQN`zP-^;C|li z)rSBq@aN%bYY~eE)usG*sxC<{!=fsZa}`@xko9ugTQ!>-gXg~7qZM%&t+z`JR^YJE zcK4Qx>n}^Ztu>~SAO|yB$2Q%J;eCE3&)+x|RT}x8c6AOzE^Ea%qspk-)k9X^e#C6- z7t!DG;9nD-c;0_(i%k(5bEqV~SGan+V_0r`I30naE=7Ku?O4{Ak$;n*3RN#yOkGUM z6=%8>to1APHUcvC*tZ&LejGaaEW}Z~jdE?(x7&vx5|Of>lDA&0q@x&d==8>W4p1bFYIcAN=I0Z#XGLN;QL6ANTPUd(%$aL@gHYrDk~Jb9QtZ}Wwp+U`w_akPT>1nIlfF+ev`-~H}(L8M<%ZB3@HPvc3PCYUoVevJQEzZ}cQ*h!gdpTWP7aB|}640N_+pR{>9FRQ}g;qY_C&us4(f3$I;!?OXSxi_*%JEzB75)h4XEdLlFpGAO`>JJNO?@ z{~Q9Qqyb_EaqeJEYW=kp7NW=$J!oym=F43SQLSkK+V(G^&%4&P0t62L#SI>jT#EFs-1wTD4Ce5)ABDUcEad2yOD#IeyceWc}#1= zZVmpDw`6gaO^Yg?h;?d~f7_qVU?yMnT3tN75gY#s61~@mG!N>a3c1D)EZECrjov$jH-6^XtFZS*>M%n)Y|%p*8^@X zP~kW3w}@%R1ZO^yo-jqBDwF7_Nd#JUQzhT%T4@&&25Ym;us%}TKKzYoc(BK2k1;9l z@B;T(0owCHTU@~P*BvES=9md|RC35t{gsAhie4a^zHQ#K;g(#fPMR|W*uAzb?_-CK z<0k7 z!O1*)Q~$ERjph8uY&!bY+Y`9?I@^WK%e@g-_fxIJ&N{k>O$%8yZ{O})FF7-@ifhQB zR7@na%nvL-J`9!-=Ia75-_U;_$?9$HN2h0IxWu9^F`Cu|#1=n$dIahOX5wVeZOj-r9xVQ(W>gzfvcvM8NsP^Ts;hSpta`v zS&a4B!=gRKKg4bxr4F=izvHl5Hu}eh>Bdlg51n4$04gq>aZ`{i@^mMiQ_n%PAE)=* zX}V-(F+imH&X*17&`&3F>IhQG`(dezLRv-yJ_8Kg#fTZpk|`}6gahM{H&W5 z;AIBLNJtu9sVqDccQC!@w{{Qi52g=+EFqI9tM5-%{XUo5Hs^f*QH+?gd!^-N@mr1! z^7&U76KizzhRJ&YhXCYt9bwl4m4b-NM7lh`&%&ChHdY6_Yh#qIxXaIX5xPFdvi01M zq57zFo_K#PEYIniB6ZJ)2kl?Z|VjVP)gV>l43XhOUl0 zX3gn)sk$cWl>9!!WDBtf~bMLN$@i3X2E zJ7|wKHpsdN^vrE#U7e!!ywu$Rb!A(#UWj@8moEX)4t}^1asQ!__ROy~@vjUje2Q-= z_WoKvzGFoq3D0_)p$&1L&$6Dq-Iae~kPcoZ*opteYd3ju$KXBqTGE$F-Pd|&@ZF(0rcGpNjx~s+cLZ^c0NG#7UG|ZGA zopd7XB9HsujbMf#7_p2PP88$vGl&oxUp~`iAvMX@8Nl3P$R7l3!9&Ld#ZbG>9JJsApK z@mqmp^A;H!n|T6u!Rx1`uhUjnnYsG1SNYYlHrx}dNvtIzok1r^U;NI@8idx~#Hak` zG&V$#sGKmp7kDpoxJI{(tsun)7#V!~UCh_$U81P;rvUEAg9P{P5ak9$jm3pNBV?Fj z1O&Nv5H{pw-B+3cDA6IpU%9#;osB9#kTqOtG@9v!E>hn8Az;6?ctQOd5IlNsWYf;U z-EOI%Q7!60n5ESLMgH?&qf(lG&-g>QrRZgC>oCV}>zN*-c`G(5N&N6 znc$nu^ZF_&sYA=nBwa&q3jj*H;SkdU!FA$AHATYudXSlCH?F5!xL``i>uglXUiiQV z0S|Jb=v$IFO#|S(hfN5tLEI|N{T}fy8LviVAKjB~NWU zQW<8Xrj+f1XQGHrLw5!KX!}}O*ou=ex~(xc;%E=H{h*@nQJq{sXZWCQ;h0tA!19@s zu1`Wfk(y#Kz!s@D1@&PCGuf0&Z6`AMeMcAh>N{{6ysih~DWL;tgn}}7QHSV!QfE&8 z&W357OF&zyVMK*()!7~u*CM))wUv1k>;lkOI&4=w;B=HEEWf+7-wDy4sN)5jxXw>8 zc8(X}ENu7uBfi8)GoRe1VoBd=yXMu*Y3s~tlU64;@$9l2rvNYu>tJ{s_XUr9H&Tg~ zQQ`ylF0@vuU*0ZYmNn)zuzi=izxs78F3m%%E+#lCwRcJoj}%viLX{$3pn~~z6iBO0 z0-mrX;B*J_O4Y{?{Oqm8d?EJ(%s*p)#x)XsVSPSi^H9Gl+uy@8cs1pgY28x1D^ss5 z}7E5r2p0tJt<}TqB7s!=VGLfguhCE@A~x73qbxdbRpl-#_Bd#dw)806FWG~ zWtMbO3rdlS_F%hIqxCY!vDYqn)pqm-#Obqyc@0{# zu>LB}V#mu9xcYpq#wWFo-uYag#0#~$+(cvO%O^=eIipd7!|v?K#={5>z>!0QPe?v7 zbeJGG*R#z-5f`*PCFjKo`L%_YkKf1P1pC%Tj`kUBbpf%=zI3Y1>#n)ItB<*op_*-u6PV?e$@$zG z#W02S?`w*}4=r?qamT65-J&rpTEW&Gqj zMPMi%$ zkT~Z1<^OLTe0Ok80_V%5140(P`8>a$h5r;-Qas|9T|E6v@t=O-|LFlf+gRZ23>sGw z{n=S$BxEz|9Y1ZBxagOJh|cBh!1~125gLFJ!WKE?Cec_xI^j z3TEIy;$s%WA0V3`eq*ugHtDJQXUPZmH{D643R?!+Yy+77#2AK)ts#es| z60(qamBqcy$dJIgZ=QCuN=|CqeJXuJ-er;N9lsC0P_Lt7_~NjM_YJS4#(l_^qSq3@ zS|0V#0Q2gDd?urHXCSZz9+%Ql$#~Jznia}d)`7uTE4gE@>_12MON87W@0i&3fxjGzA7rB)&GFN_KWR## zzl`$*%baQb^g6Ui9ZT?bH?;1cQW^U^AU?`OR0(Qr7oR=p-AO~0d${LB^ z1hTZN9ULVcsCxwb%(>#>b=%#VyJ@KU^Ndr8f)acD0KY(AL0X^;qz zHY!M>kkD)VyYd}e2Gus| z{qCrP`UvRW@W+CK^yIDT#*B?9Mo$QT7A^R<@N>Iq9v;Cxd~3gkZ~bYTvBPhn-uGd;TmHfJaTdPbQ2=U|6{1{*EUbJA0Fk*h_9g;qLZa>u;| zV8-4Sit?wx^X@h5*P948{HI;tbAT#Q5%n7>Y+xgmMl%P59-AcmDUf5vA3k?g`HA84 z3c36-b`d{%i0wf+9AYN+%vO*x_+=QMy@5L9KM<-yZIAyxPoWv}aaB0LVaf(tXis4P z<5e_GaiT=dG(g=*^QXW5*E{I{`1N1pcg-zKRu?BrJ}s*?u!alV-LI@$z-}GxRj5S- zS~ZG4{OmY7vVPQ&;ob4Fa2q4#MeOL$9!GgvL#~)^rIxgx8nx$ zzH;rW;qN03@9(aeYMIrLx0kh>E4~bnX%17^K?T8sjk{aE#WE;^INZ@G zcy%giY-E36J90<+_w(NY{_fd&D%s}E(tbI$yw3eDn{rL0Lt`x|lU0B{F8_GomSfbu zcxPj8N?XvudDP@KZ93Wg&b6>kajBwEmFvkl$&5-%)P;Wvnf|Jh`>(fu;r%mK6aX#} zg`LFNNiYAXM^z1aG5&*iql;g|^jjN0KE?b87LE)m6V+y@d-Pfe^WTv-{3qVQk6X;` zpNx$D8yjb5sdM$t5N7=M-T%hWF=OCmDun*)6)-sYkeYp1;EOf*UL-#;od)2aA;TA; z{OjNKKmE&J>!GK0oI<2UEd#>fnYhBG7xA*&5t~H34hz1-}pKElUG)p zh}vF9oGQJ7ggFMdCVpq_ucPvcbF(xAQAW2t5)Lq^Cv10U?sz@SC>h{56 zS$<9B0Bmz_WlYHj%)4_^u+^kE#9!|t3FXXIJ;L#e&|ta}ufgO;us;;rfui=&P7?=5 zF@46i8)+KUM4g)*lbm86!GU!fl7e6$tB*@V7Ga-7e(w#43K~N8TN{OmzHf;U6Vr7L z*=eb{j~&I6c%U8ggMyr%YUKK*PZgUoO21cVXfHL+->IBdP@taR0$u>Y*H8PaPSU?@pb5JdmRhArRD0b**Xd@!lbUyy9w7W70IvjhQ z&eiFo)d5$?KX`GoFQorW`^KJRv~%s@%cm)4Lms(-u@>~)`<~_N{!5X^ z=j(G+I>-FMLH%tB?*r|radk=qBE-zm<6>{VJmItBKtKP@!4ch6i;i|ReB{A1f2eEJ>ta4e^@wSVfp?NsMo521h_#{a)t)rFnHkwlj zI49=WyX12(ijlEM+sCZ#xOod3{u=Fx3}!^x_7ymp4YM)$tJ~|8{#Ti~G7{X+q|@47 z_JQF6JOb^*V=XuT+jWAMf-;_4_8saH`E$0zUBo&v@e;K}YQbGKE|&p@lt$uZYSh?- z)l-F%KxET*GO}-;VX+Po&Y-Q5dg3@BM-SCCd@$A52x72H|ku;upgR4{Ze__+QxQc zcUNS=GN~<9%if_cWLiv-+9hwWgLe}BSep#b8anR=Afr!9j#CV+6 zqlRLmo8>IC!bYp8#-@EBm)xW&F6KXV=&Aa5eZX^~xa{YWwSWT%kzU%A(g^`# zLf-`~MU*Z=fCvOoA%s2>ng9W!C{1XE009y}2r&}+E)WR#t$pvgXY8}jsq>Aozk9~r zcZ_fSleK2DAZxBUv)(!9^FF_Ttm7T6;&*zR?47*NC4VV;qu5e=&}BUFS>%_pUN#K& zM*rN_{8OR5dBWK#_IMEqSh?OOOYP|G@FzCP>O(qU4wK%3jdELWwY; zOJHB*ro{L~)zJdH=MU-&kP|VqZW#9-NWFQ{eD($>yyIJg$$|RROQ}apVg{4*39rgn zynBBfdjcC`FGnW0QjOud1}7mGJgkFMv_M3_4o{3Y+2W_YL8Wo8>V0qrYj=!y`P?&8 zN51&<2jQD;(Tq&p!HJH8K74JuBvH!XUs$({&)yvhQeuU;NtoA@hJ}XU}yE!s;_lLSJM7Cun=M>RY#M!Y)Ogh4>}xaB992ve9E6kokGGxG_V( zI6!fL7*gU@oKTl@j}-21_`EK)hH~>*CQD*i?_4sCj-h<^IW!M-4m@Prigf`?|$#;M-KP4M9-i*2lVpIL}xB|m1Pp@ zm6XiuinljLX%jhd>V2-Rr7k#8J0v^@p}Eqs|0wowV!mrCS$vLK@WMwA?O88s(r&pp zwMBma?NfJW?_2Fzu)}80MnIXrP23pmk=(KKES+udRp+s)2w--gaKS8;M;mor+XGcJ zbE+%igYM)JFc%jbP1O@KNcOc8Yqk5$EfHft(h-OLK2@q&3 zo-9+#b1OdlA+lUwct$NIJ|xz-W`p=z|4?5TLe3zjrh>|)eS+WB8bejk0bXODk06_$9=!++1y!kXr?#Q>~x+{^f9&Pt)c8(jCAE9(@D= z-^QqLL7$yf*&+wh9>V$OmzJiHfw|zqUWgJ?_hh*OQM&oyCcoRiUJ8{0_cMd>0?nfM zmhm58%9(CC%!l6v6pCgEv0}R~w251F~-v3o5`JFui5*0vb^o9bDL~ZP3IV)lkyYO$k?;%d@Ej~!AKetN+<`Q*PQ5T>{sk@ zp*fTXMNyvXhg*z#bhv95Df+UDbHWoO5qPfrRXgG5_F1~4!%wvR>xApHdRjmTU@aM# z#rMz@YwqxoSA6fyT6YT8=q^|B)IQ>xwLl!@55JKzBSx=P8s;YotVgch!9DBlARkL_ z?`mfU^~CQmf?LnnW>z&eKz8qj^?;p%RPdhA&){|kjNA8MjkM9eI=n0`?>Wr;; zPtte$)y8XX{)amVq+|a1_P>O}8UHJs7m)k64lNM zr=n=)?8i0~!bf+xQUuow+7r>u3^G#1_kAG}ceKi;Ukx>y?V>W?{B2P=?B!DL4C3};#pOpgMB5%wWS{hZh&HNzI>k1zWv*#;eM{GA)YyBO0RyK~UJ;9nI42F01}RN+N=VSmg-)u_ z0(j8E!_W~EhOtklM!Q~#I&rkhQtjIe%upNM*{X3AKDWO7akGrj$PD8upC6gr$nc#Z z2eud%(rc;}20nCs?7eG!tqE6 zo!#mw0xb6UUS24`uedu*a-=?lFbXPm#y9pgue4^)FW*Z1l%SuKoh=TV9`beg(cjTb zBnocMjeuFZ=bGn_39RQonqTZ$(@Qf2v=rHKQ2G4G_|dI(LQh47Iep| zVF}7uWbHyOPm#Xqz{_Rx3I!$7caj+u4|GaFgNrMRiBCQ1(ebU*x#RP{3Z9%tWRj6z zT9K;dPe?K^J2;YPlxC6w-BCZ%#v1B+2ez3G?I6YdKzjb^D}1-SX#L6zVHJL@krdF| z-aaF3WOT_}0)^^=glz|EU)uBF&vOetKN39Vu9yotuqL#U<*&=S+zv@ zsT3pQ1mcXnLi+d#ppDVNq+YfI1u;pml6%YnxB%r;+e?_7Tu{B&|f6Z|bF>-s&H#Do`(CSw>k@N*y}Q4XLBC`I0e zu=>_?Lv57HvE%M}Rf-?1R4jXdCHqkJs!y1qX3mbann=#Q6cm7M7NH9j?n^HIF$&<5 zdl1@-K?35bSxoq~jU`HVxlw71fr91bySsi`Tf(=U;@5%oX5IBwUA4sSj`E|9c2H$x z4ng>(jYEfnu8Q^~gCvvUhtKDqUp;#4Q5&VV{e4yPvDA_QN>>&<<2p67ddPKoIhJBr z=@-2x6M=D(7jAE>k}gwWuBvu~(e+sF^*SbI=7tU0B2z;E`Lu6XR?Mvaj16)u3;Fq~ z@M?4A=Q();w*q_;@<+1^gQAK~5Y$so@Q9^{$iZ}Qp=^AA7Yi__jY)WunQ3YttIISM zE-8rsWZ1SzNoz-`Jk9#R_hMx`72oF8c2<(<9dnC4`Ow%X%%!i~=I!uUuN(+#V>NUm z=;4B-{)JAQA1IG0QKOUSr5|CLcv9UJ_+*nuoASOKS6o}t_n zVSS>9j3j>XK0b6>C57^xDob1s~uj&>>roN}RcBP-W zR@MP1&ZqaQ(~(|yLoHR;>mEKiWAt|BwJwKdzmH7=R$Ow$a7-jXD++}EDpt{|oZizJ z2>aZT(}tHBs_F_;yg4_4Ea+Qub>&5DuX@1lX5VcFUvRBDzNZckEYtAouy8d07{xPu zJ9mg(_b{gQH}zOI$Xo0accc7UZ_i-f+((sND{p(^ng~TKHueJO^DHCp7zy0W`b@|g zORvT4c^T`}PAn-Y9RfeV_xv^eOIKA}Z!!7GVvHo_Urfb+&R3P~$OK^1u>-@_GR`L3 zeoKLo2#ejFz3O22s?sbj&$z2$tSPUKXa{fcw~a0B>_iLiI}}BU?-UAaYYH=ltQLAK z={w|=!xz#z;1QqalnceZt@*bfC>NDycq>g6lYD5SenhFN$-Ua(HNM&fAAb_i&+T~@ z>%>f@=snNWnMpbD2#929IZ)jo;Q4KK)EZh7TQ z#JL~H%d5QqI_;e*7OCm!{W~pXwZX1q35zpqvn5E&ZlPbrP}gQ8-W@U4ejMcB=Ke6D zqNJycE;+#;!)@j7^S_ni;(OjkWZ5Oz2jH_P6e!HxNpIN(3L0;`)k&WDLK84xuZvzo zAyE?VR^0hsz*48Q!7mlTPZp~*_ilZvo!j0o&_@(~sew1sRlwM+5~Dc{j@8OS$-Kn z&N^t@ov#Xa*OkMyE?lApB2J0CKN)5$m6zZnhhhd$o=H2p<=x4P&Z&oITuBH?UoC)G z0arrYSm|JHz?!sW&sIc9doe@aAPp_f*kQ2e`8$4!3j6s+AAb{Q^IXNG@SH`wJiWSe z)gM-XMV0h15DbQ%66bKKjtPKL?o%JWa-5k9mf9-gkgrWT=4^;M__e4(E3nHhkFLhZ z-}cfOOz8fo=RkcCbbS$1Jz{5PZqyINcX4@J6MlGoUq_iU-L5Y@K(e+m$s4e|QfoB_ z+o7rLjo1wunMP|Q2;5hg)fiz|YtOOW-faXm^&aeQDfDY$6)Ly27_$?8kznKCKdN%( z3QnXY=>=mmorJ@r5ZG6PW$+B!r3B!%2SS4(V8i+WQy>en>fFWtTdIsJ7OYgxM?^X- zv2i4Xr2tu4f{pT{RTg{)lpp8jIchm-%OL(z))(Gi=6$PmWL_tnW8tuUnf7ctemOs_ zO2Fn}bLguU?J$k19Xhnq*qY}!hhfR%#xg{kT`GdHD_msuqF8G zAFMyH*1D1 z4If{DL*G_V(+$-49)GUpyKl;+_{tlnSzg#J)X^?+TI??_Y`SH>{6rg z;v8Bg-snrz0|Vb;qm0I?V6~&kF$lGb{ovyZs|%flvIm_3{$Wlh*ea&}E~893yKTDB zNN{Mgs(-eUB=UT9`(clUI*Wm|b}gp22ooz6Y<##xUhA)0tM_$M8{;L-erU=`E^Exc zbrzGW+{PrR6cc@G-I(GEb&FIqb8#?#u4$o+lW-TVb5~LmV`k+u?(n&K41gC4w^0;o z_dG8ZO-=YMsi*`dR2=S8Tc`r_Uvr!X%_M;#)hC?Mp1gQ-5+J>*T>1l4XUW+kfACxJ z>kN&vaYBEz8y<6WgYo#5RiEzK)sUqhmXB;M2JQ!bjC0XjZ)y3fbKME%0SXGRbS^#+#tpvB%{hq?$Imk-SXy*j23EcZ-H2s2ZDTm0?`Jlrj*5uN=XUg@cgJ7? zGOjTt3{~63xZPF*B&!8;s7DB(=k4+C2`xH6MLC^NiU~Tc1(Jy6HC(f+3%?m!+R&#- z+(LWOp0aBBqK)p+reM&9Zmeb}Je&XQ^mRO5WZ76&W+YBdR7aJ)V-|3##K5>zZ$aPq z8@WXHL}UI%=rQ?2&8e52Yyz)kS-G#)m+nVIOiUdRplyS|KP?8`i9HXGHTTd`o(UXi zFHgk{*Na-8P;Fv)6Q0h_GU|s#YSWjeCJxgc&q6cjjMa2v5MbYfhmLB{N|h+%!;|xS z2R?l69+a~5=+Lvat?H&YQ8Zp#%NgE#2ctsCds$~GOI=(<`9&b~ZK?mV)XurxoA2%5 zc!m9Jeen}4Rch~#r&v)>qDMZdaA_Sh4fz#vDCr|xcd_ZAoAKqFkqvge&p6d33Ssi1 zTg}^1Y1B2mk+{Jw(pdKB;^KLjC-OzSqxZ-pgXJ3{$+$?h&`SiKhh*>9nyTv z-wT-7z*bFGuKXHs0rPp59p|Gq&_Vvq-NNww3HOH7zbtW4dwtc zF!Izp^+rjWA~NKi&pRxJ;8#9!C%v+*{W6w5SxZe?n`C#^^AW!XV+z{u^Wh(MAnMB> z3UK147NH%Ks9d<8$u5+$(6K*u{$1ojoEjXN zD=k8Q*-wJ-3>Y3xvLeTmGy)@rJ36PzY}2c|-SY;_6^wr>oWgX@reW|=myG_(!NxsD5+@b{_wX_U`B7Q#qvkyQ_);~XG zyXdl(FC+S6|IS#^B9XIAxq&O;s6hOdc2*IqnqNU>kqhhVkou;Ic;TB@G0_(no(^qj zwIX5y$@-RUr+!gi=SV)^64oiRcYhIN+3w$@!Z`mle-h8p4bppP30VSyd0l>n?%jK^ zq_Zls<1g81;+DPMLWM;%ayYqXjr+rEEbkv@(j1UYqrp{$=#ZIk0}0no~N) z$sdr;vyodQPy5Ph3YL6slSZbWv;5@iTo4p9l+ZR3U7T$;6e7=6r>J7 ztD}~pS6fN^7`0ylJ|I~0)|X!T`3s;NrWeY0yb-PhV$D1Ka$p?j2{Vod9RKJQTFQGJ<= zxs6EaJPIMi>2Ypaz#9i^3xB0tH+wz@pyl*fsl~xNGv*>q`rMy%R48;{bXdAxrdE_j ztFRr2ITvrNR=x86!J(H(JD*8YkI?JP3Z-kMO`3g{!S zz9m%|cAXK5U1Bu1s2f?PvvEV>7%X55c;c#ugX+(yJR_kDaGp7rxYg{3_XDb>f<@3+ zZ7_E?dDz|m#~+A1+og?5Sh>G2@=?$_!xo5-XF4bP+J$x>dH`JeN+Me_qQAt#TqP+5*phi!pxQAdz!EH|7 zqt+BskUJFxwHU{;4u1MY<^7+8R)5Oq`k$Zw$L9=RvZBCBd_L(K;F}6IH5rg4s%nMb z4Ep(rd7HUr*(`s*Vr9Pd^nteOVnr!a(c-yh8|H!e%`?f1`P+(G^f2QZm+JzjPsb${ zV!kx0%Bwowk}y8s>*{T1;jfXD1Pjz`F_ySpK`hdt*X+cUV`GuYZKNNXSPBi!4rx!l zr|MXoy2YV&{`p^y+7&k`RUK9n2?>t*YAr2gjkztE{HjUei8;h3F=I0e9i7!`Vrn)h zV-2SA3j`I}6&|()yNSc}4Rjq|%4o>ousk zsCqZRIq${eZun5s-B*oQ9ekKkp$esyx@HTCxQW?`Rc>um$qxxK*ocWp>1b6O!bqQk zgRcEu8gAS@s7|a1W!~PhL>}|K_t{}JVnF+tY!*BC;nl^C*)o-X8c9RaldZUo^ z$zWFSPHT7yQ<=Hk!wg8+d^rSv;&{_0-IfkAuUvw7<1c)E z)p&-f@`=U6oHj#tM%1kf%_&ZM+fp+E!b~lq0Z-PC+(Ep6xAM1`^XhtWvZDGZocyz= zoT?RPXM@DAv+NUYvwImByC^F7a?&y+H9@7_`*&A_qk-C!CqxmN!#lpG98072(9?p! z0x48d_pJ<;ZjhN;T0}R$hiyx&ksk4Wn3&?P7_MgQ1ceUNHwCw*?qJFqyJgb~kS}`X z2D1^G8h=!2)4T$mxtvJZouLs){I)Ar99|5m&C+_2RBJ^+8g>Y z3)6ANX_W%UF#w6?np*rY6_Obo<|F~XLJ+2`1YOI>Nm}1e-f*DV z>G2LX6nBi9lIV9xC`YPAJNEQPjC4KuzuV=hzpQdN~sBqV?jbL@nVk*4hTR{LtvF?QGw%)s|GPERpU3=Iwb&5 z>p_iU7y?oq?jPtEnKeGxwf2gEELwLc3)Py@tIV@a{VS0d1X|P=5L%zFk{d)?bYwu@ zo-I2rYHe%_f^eB((Q!k7!{m%L6b58sQB@rGNg|Y>hN?|xP=_%_kM~sZE zVD`#q-%?5kuQ*n=_J|g3HK@)TnzgOAyzu=1<2rd=DWf!&4daxH*;`s)hPN3x^!5Cn ztH)H29yL0xQ7A1)MZwWA43^eD1E4%Z*OWQ)h|5(648J-h`IJ(ySB8HU>5UL;%X22e zh1LV)V%4mR9P2bw8>$&e&P+6_-8b4K*^{*CK&EAA&3gXflwuzJWY}H?4W};$%W>yG%U)k{P#7g28dt5?#TkaIMn7a); z>*r?%4eBx#g0}3ime36etIFu;B5Q$?0&7@${Lh6Jz;Li;Gr3R)zZ227`anJhm5y3;$yBK$S3s~6PFs@Yp@{H(Z zF7xQ#{RxDND*`cU?qsG)L>My-^@S%7$`hOXiH4BIkFixQyan&)Ka|Vh z8W=Gg0_Uwi2dAuLJ>TG@2n6*<_dHwKwOQoGpW#x5$gT~ouRglnjh?rpZjyXmhcXx_ z7z7pzX!5=^3&0Q-9~QfxbUeXdK2Ex~?`&dLrxGoPmp;x<&BHo5M8GqnDR1`{n<$fB zO#5M`u%dZOpxTGa{7AK5g-0GXX!`QqL+@6u{EvpKsS9ngkXW*fMud{pmb|WSvPEwP zJ%tr5)R*2_$g9babtCKj47AZG4_Ul7_afs^XPgu=PWoMyWB)7eJP9|2@u03uhfyv2 zxwnuX);3M+j!R;skYUz#@L-o*6e^Yu4(j>kh&#~h?PY)TY#l4RPYjsK)CjJOj2l5g z4+J}(6pALdf6BA%th%!~JnU3@8688yGNYp7phUp1^v^aRfDmr}hy@AG?irmMM&L?$ zq@}1Se)M;NPLr31mD8+sbr5(}NfA*jLF)f&P#8e5UjE(CWaRbW)5{fl1=hc}8KaHg`Fy-lMT)Z@> zO21Uyle)vhWF4V-X$E)gD5zOu-59Qnxz+(MIwIuJ@DB;{Wmms7b(N_v730n6u@9}A znD?#KqoZjB>48$(!488I+KaHuZWp}EfP9&s926UtOd^-#fYp|c_jN~38G>E}IckCh z(Z}JwI)f-3IfuNo4D6-t+B(tYtOHk9Uo+8uyDqPeOk^ffS!ruN{DXuIeTJ+&k|AHe1jHa&@o$t%FFF47Z$4J&mk@xV^tLsOfinkB9BIPMe)+}Rf4gSXL zlY?NXo3}gMI3A*7FU%43X5z77TM)ux*ps}vo3s2IdK z4~aK?We`uaCWg$%D>~bsqr$E5`I}jET1uLCsH5=N+<0B5L3f8~@2wj;H-X=B?fU#? z?HTQzLx|(%q0-lbS;=dJy$k8p@@Zo&Iuk=Ms})3a`c%two=j-yQO)eng@cq7jrtI$ zOa-j{;AX_S= z`}TZ+;dylcbLYT;r5qP4YXC$8%ey2y7?gAHlOHecmzE=vO=uZHrsVDMx$%&-1M!J*OSwPNhUD|u7TRp!ln zwQ5*tseAA&+l*=w9v~r?Z%H=aS~OQfFAWP^ZNON2Gtg2`^ww#j@%psD5!2(E-Hjhq zx8$b|nnu%yrub+nZK~#KgH**BC)tW zfsjdw&T=El1i3e4>!90`o1M+u9Gy5fEi7%5-{wSd5$D&@T#;XnOGF@UjXblGERxMr zk`kr+sAu?)FsNjEGz1BFmF*zH_-U=^G6UZWPmnJcaPmi+#e$^z8`{n$Lc6Eg2ko6#5a17fsB+0ohZu52lG!l~dB;iC6tU6LV2Et^4%DqVu*LI$<84Pz4 z*BS)$7CAEUjzIs%M?Gl4aA6ds^h>k(eFq@Nyzg=?R#$7UD4nN(V?*d*XUQYma|7zm zb)EVl<3DvIjEj#J=fdE0TRVuWRc!I2LnaCD`zt0i)Os9QSF-0AOs4=}zBfA$Wb$p+N^G`45Rwp`p9 zr}8c*q^{>j*X5if8nU1O-Aho|#mS`ixglgEV#jL1FPYh>TZx3S%-}dDM8B;~DlPNf zHL;j!+!b`kmb_v?r`Zo-KHe6u#(h+8W*BM;k5|nHnU7MrJn01L=-27@YoqHUWxSu5 zv@6f$s_2qVa9U44U%WTJV6MR%nf-CYU^I4+Qmp0aJuUIZ+m>*xW>R(1)Y=uai=|n2 zqRN(?Bv6FjM8EFQ%O+{b-rm*?mY_NnE~Q(a_w7X1kX3!UhpUfZpqqPo4^=sW(N_;@VS>J*>=GGZu0NSN_N-%eB4}7@0(cK&g!^H>xnvt!2IHsdSph?{-fiJ zzi*UBUjKc`-yHu*{@LGr;=g0}uL+4avR_u0Ekodlvr67wUc|cVcJueuNw`&5cxd-3bi9_BMlqM#vA||KEadD;O^WOzrJA<0@zrHl>*wa zvld9FnTq$hOoSYTcr&?Rw|oOdi~bib`3>JK+4`K3puDVXN@U=d{t2vYDEC$)?Wf3; z*!}(TMKJQ^_e#N^gKRN(!>)0Pmltf` zQ1yrMhw93`Sg+BAo+g-u)XS$B$NcH&B}B;4tSQ1rccTqYrF%IIV{p4pg5ATivx$&a z*b~}-ht}qVS>NQndHCq>{Nu5hJjw7V()0+{b-HRgYYSh{N~=y`?rP-C>S^SDneWJl zvaGeVlmnW`wiQ_WdrFt+t_krIw5KRn9;vx1kL5e;#4-~NvCR9_YrA#f;F(z2*(@UY ztP71i^IFp&-2Nd0sc2+;LQjj;N#=0PO=FR^ODJ~&b$bbFGRWRnY2R>*vvVx@(8Jt` zoxYN&+Fiwur7R1|fQQTi>!~&#;3t3o02INI}&h5wH%6Wi!a2O_uOwMLZ7XmjkNLB63(FnH(A zPL$DC^6G1i|9cAJY3F_`E3?a?=GT%BmtA)dZ5VNh^H014cnkdfJ^Od{_`kHZe~-!g zcOUxSe(*mM(vG=pXg@SG3<^BkS>{n0s=ppSe+Q>2V;gl)HvL_oOR&F?1ET1TH6tz` zHIt=ywT01;cF9IXqJYzaU(67Vi~{=eo*p`xn6{%k!?c*EnS>jAnL7_=WYJL0tvFe(-#wiD+=VEjh=K{w+~9nG>@x5t#Kobuk9ie$cgQ9 zX!(SbS29|FU%_j((A`!rJ0@rQ_QzEN#AOg@@Ka|o+>iBOwz8vjZ!K2GwDT`mQEjYm zOcs~aIxV5QGojSZf1F8K)g3p8b<=*&*JHAE?OtM{sdyr!Q6+o<Cv8+00iMsnE8bdsC&r9L8(x(zF;CFX%gP^FIB_aERyqwQ;o{n(&j^{O~sjiD0W+ zqhLptJduf@EDGNKcU|q!e~APCKL=p0`r6=j+FxvIJ>yEEU$skVZcrlyn3{_%%G$<} z6@gLWvglK$j-EW|T)eehuph-qD*SqF{r(jCk5B*Z`u_IdAF=27+w=b$_WwGM$fsS4 zZ=)iia`58v<&-lRvZjUW=rkn);rzQ-D{5`0Hy=|P+;FCEb|(8aL8t;_D5mR2Z;0F_ z(bu!;k5L!;-xb1it8uVTI{XI1sx7+&6c0;YCDB_VK`4}NF$TETW777bmg&3MCbHidg6Q!LNpHL z5d?Z4y)d(H9tMGK#O}qmR*~zTy<>OuwRZz3zJMH6qO^tRO_C7St(77N5X5SEtl(0n zPH*!fy6jq6CN*yRes0|R_14FIJ(MRXg`F8gbYym2N}^z)!IcRD-F`Zu_8 zxVC<@5{Wa=KUGw?vN8DKqw|&gTdyOPdCeHc)Xg4(+FQE)o1gfGxr@jxAy?SYJ|G-R zzQS{HKw`}?sY25O?#ZiIQ}!}9LT{PL#jCRPJh;o*?z;UeM=xt3S> z$LxdNEO$wdQ<1j0)srV96dP-)=rCf7g=JjQp5@QSgd+0wjE~VfXU)FwJ^f$dZ*{cd zkmbxp{a^oM&Hb~+Aph~3{#(gEHnsn6r~h~C{B?#^n6YfX_7IH?QLLww+zcK?txuDY zUhk`qSn;N6ALNCPo1E>8Je&8>HmlhW0$Pe*zl_P06$@jKYGVCm)tmJV3 zO`v&RAlTrfuO4e&m9v=n4zM^=Svr^`|7EcF41rR@afCFkOnY>W{ctkCrJAMaG{w4v z%3v$DsCm~u0+C7ed>^ENR>$gpJ&2=~`M8w0k37`pOfT0kW{X9GWO=YD} z#RPTu=9o-B&)CY{#_0k_RXaH%_q)I?y3rB>kfJg1W6@C4(93EL55@S!Lf$UXIYT{>RcTXfIopelFKKrzON>5*tMqb2-^}Oi`09{d^&Lbafm(*m#MkK-sU$_{~Ph)z%|M{T8YEE$%BE+sYLhv?q!-Fbx?ybytZu^_r=p zFvPoEU_ME z{IDt%gLixwV_!WQE^BWXHS!r|o3lAxOHcF%#751V=`=%pD!|$$&NoKwh$0RFtmvzy++~mtxa-!1n5F0D=NGMok@rgub zLv$n}_=g_f-x{8m&bbD{ll4RDxuY#pWSP*qxl>ma{5S{4r?h1aeM>w&$B$QKW?DBo zDb^NFipyQfP2Z@D8L056__7|s=-q8RKB_87QkT=|K22nrcbFUNyR5NU@6-;NfgB%Z zt!cw;2LAX|-0qNiC&O2~#b5PQc(`GuO5ozk49`u~_a|={vAksYA=Fw`-D54o#Y&T_ z`A+{v0du$W@tmZQprH#cL?JoT&U{=`UTmq;bvJ!_9IrQ_@@`|TCa9|d0<=iz}4gxg!6*&%?Qxp$8$>S zsP1LU?*ee+E(R#^eF6JE;B{C3Bl_GqnbT)eRWzyMhIKzK1k?~O=h$^z=-ydLnp>++ z(QLjQOWG&v%DmtgpjMA(d+)+G9riXETQfi!967juwc+&5m@HE>8S45M2=VCdhN81( z)B19TZGzVYd0`xpu?NnResvslbKTmwu(4mhu(9fFh2Pf$JErIT`kwMFC5l7G*Zqd!`{B(DFavj>}r^|v3F zkdq@SUYb$e*UE(u$MZ1)Z3BYYv%G-CUc~YI8*m++O+EjnGkEFlVd_)TR#QP-lhqEw zAk*;15GIpB)09u>@}7l%eiL;!a%=E6eaM}R09$t;$(<=45jDsW5LE1MpR3W8Zk>&o zQt__f0qSV=rZe`$B$wryM!>)wQm5UrW6}{WJTT)GsyF)}Gt1Ww2^hKV@|&1q)~C#O zaxJX1NQlv(g<=3M9ZLJLzXPjls;GOtuKoaV)@)4LPWshX0)+Du=$LM&*0NSe1;4DK z9#*!zpilB*N}2Ved+7axfdiXtc~swq?pVV+g6e_3QXGMi zwy@^TJ8Q#por$DWl4yFl()AIPn>w`WDlCwqeMGHtMmO4QQq=uahjZtwuPZ{!N3$_f zb>2+lvNm0t?KtiHm33!rngDzdS@Aay|T7NNDTS zK{-zCc~9YG`{qPHyQkQY`!u*mND&D=5(jOBb+SeZo*>c5jMCPI@uL7ApzfXgvA+77 zWjJp_vhpABjH1nSkEKio)UXroEd5MwJ**)r4G7^|fh5>U={XWQlNE5{s(`@hAA`lt zQBn<{lZ4j0-G=Gk1^TYK0B0G97eIoM)K(g7iS=n;+D+n9kDa2+AdT+{LlfZl(I}3PMA72&KDG z^XsYNT(Ivm+S1`;jw)k)2kvbwNVzQWxH!B&zUq0ud05pj#xAzBr-w_|WQe$nGM!V9 zNFTNLeV!ICunqo-m3}Rqb;Si^VROupOeb8LTL8XEV+mc)l)D62PMIHka5S{1hh{p7 zjfZ3`rf;KcY^)AGp%;IU1`owJ-tQ{z`4-aW>;QpdLb&O?`ed*K#jPsBO;Dcakq{ri zalD~_?b2J4P%%1>d>(>0wKLlCXgy!t+wAyW1f~MqZH!V0rzS7u)znzp){s60*iI3P zxd$fGH`+?pxjo0!j^154YgDOIMTteQO8a`+@<}LUfKH@?f)}>XLDiGs zl{tXP^vFXb+j+*gE>Kcr9EtLfn!KURp8hy!pr{J4$aIL>QgI8$8f$~|E?|b^dh>6( z;@coKt*2iWFb^6#G>~I7;*v|;bLl{M*cX`1oa!f>=(?fc`N&{`qBD*$LtnYSn{T*Z zw{b<|oor^xa5#Hhzvk*rZ*+N`?k&eMuf3sF>V(IE({_GJ3er(kQ)lzW&79xg{D|kY zt6Z`;mN(~l&+HXjl~VI%eSN2?0)O!gye3G)(LymYYB(IfBSJPCWv;yJ{Dc#}iE|CV z=dQ?{n25_M_)38+$dN~l8x%SK?* z>H)Q}bCf0Tjv`zuF0nY1lByrr6FAS~S`K^{(0I$Z;%e{EFl=QOq~w6BY-x$}+xF2E z>27qjvdfHn#S+?!t6BVkbY5hvmd3KV`^l7iV9&Ck7=b=|@Mo(Dy*2 zC>O*XAxHC(_p>)e=E~tMhGSU$%k?Of$I7_zealLs{NZzo<#M)_-JFHG5kR{Gq0?>G z>Xu!V%H4CSPEcK*p}+E*W1oiIW4gSp=q&bEsm1`sD|8%}Ac7LnQea$9o zwgB$BdN9HQq`@1j!!c&~ZzBt_Z2x60jm$mt_lCs(fA{#?uJ;e_19Q;gQ#Gu}(^{<; zTjhXR8Dk@{)JV zVbfA?`ZxKL^7pMwM+=^zTt@-;vU%5L%bCs@Si|jr+gAdlO`9pSAGz|v;y`k2wjn-l zMOn8M&06;znEBoHkA_w_ERXB17}c=%^q$dJ(eSpIN?@lI|2=_N=GWHq^u zRGbKmj*BpCd!IKl6?^n$XjE$Rey$O@$H;iysl7=f6JWotf)4|J!4rnSB`!??FfL*X zq`$Y=%&Mo9(6|R%NG>nTE|itlHNu8YaXRCJk?>tgASL_~AD``u90KC)i0r#M3=bLk zr2LJ0$P7?Rfk0ZCDbdC%!y$Y)HQ#dt`h*D%*rI_fnlZ-!W$2`tW~ zBD7}~h=JOR?i>7lNzGRoJrZgf$|@8)WYQ5uib)Wt$I@JLgO}-yaWc$Sl`y{S7Sbpw zIX?Z(G&segl%}^QwC4)9aGmxCE&jG2*ftk@efDOus=m6gg>44BlJMd~9ad@S55Qz_ zf|j`fzB<>l;PQ~?NVuo8!-Lg*b^6IpE=}Lp^C{#Gbfe&DVdVayK&mtsTV)BGoc|gosOPmqBQ496o7@PoobFu&Dv~!tU2kF@*=w% zq)HdLta@3KLw@GpH}SPlm>pzQM2dDSpp#|P7gF__uOzKkN1lYhR&dgbu6`EZ^j+?t z{R4Gc_KNol20l2U1~GXHsy)Hqh}W1fPWhs3EKiJ@!6>fd5dx#N_~e>_QII8^pYH)U zK!bk3&6$M0mDkqG7YV1Z zmo{@<{EGAY{k9xc+(u$z$J|ox5WWkE*m7u&k3voJ5{-n#bn*9@3@-%(A3-%0ap6wNiL*qu!S?N(x0le9!9^@QB2vVqralN3g z(A$(hp_Xi{+u_Qk6(|NqOSTxcZD1U9_y!fs>7Rb9^dMwRa%rk;F`apZ+Bu>syCpjE z;;hbTc$G+N{+tSvcFIxel}DXw(83qy_|e&+JATpgJnd+<&6X#+hVnKli??yO+U$bz zLV`|E!JIeZnCSo0+?7T(m1S#eyUH$6Xa@>pEUjV|5bPjxsTG#xaaJ9_uk+49m#kd7{E=m=6Z(=M!!Qh4E_jws0g{%HJ#$3pPb_&== zThYqPMy-b}Ws=D#8nU-@_Ae+D@g4!FM!f@t!aa3|J`h3^>lPgCO&}0fVD0i#D6#13 z(^6rd9~{Fs%T#@aI4Ci_SSEo4&(F`4D^JEM%nHe+OUjCwk zzE_3>zi>sUa@aB_70hLLqD{N-#GXJ`hx#`PzBT<)#*94jUi`P7>r^MEfQwx>DOHI9 zq}M%TBWlC3m#AU!th}b!S4Rm_P6+q~|4 zjh`AyfQqI!+OlKMsKXWl`A(QksLqbz(eCTCm+7U!jpKz0NJ%oaZ;5#7^a#pP&d@6LYzpI7;(WyVx^7sbC%T?!xogU!{MbvUmpFk%kI_I} z)_pr&)CytlQc&^L>GI8OJo?rTUWtrh+(d?4#C9R_dwu!vToXp80eRD9ck%kzy6~QKnYB_;R4f zgpcp)V=u%XYfA(IoD+kNhoDC{GFGw*ONRU`A&cnJ3zypb0U6!1P(ep;fpJ(%hGt9| zwiaxH-y-sAr+KVpi;+9}cqoF-7f5~7xcLK5N(Y7r)bMRAV15G=x`jUFMhbX>b znJj1h9ybINO4*WAH?A>mNo@e&7#m6^RXZzlW~^L|@NmW)6m+JS2XtRP z?CzeC>>BF`v?FJsKh^V2z0ZjlNBjut$9lnGfa=x%LGshe<$Rz;F>;O`{JAN2=jzPu zK3Gl6Y7JL|plHacF^HSybsujtx9~qJ6>}T4Q-17g2@SSPfKG{c5uip1Oj|VlMC@e5 zm`YQvjX!2-36!U>pax>^i8g4YIYl#gQGxz;o zSDX+5qmc7R$z~$Ga8rUa%)Cs!#2st5UsMZxk6Z2_zm#CK1brtZ5%wnsxI*a;kitSNCugxow^3ML7A!HIz z{}=Ugf6e=$=*NINd?(Y!DXX{%#X!h7pZXNjb8~*dH!Vx5_UR53!ib~2(Fdt` zE9&(b59QTx9nb^+vc0siZqS)$mtEEl3P(N^{_8A=B)d+&*xz4H>F?J!yQMOzVPG^ zCwPOcSyL;)D|5}=qTzLS1WI!>r}}=oib-q$GQ7q0!RfCmMxO8VuqYv@t$F$DbLLhy z0a@k1Y?|WSl!6XeKB;~`D*+%1x|RR7ty_v}IGil8w8*eIe);Z=Tp|G6Ot%NYVC3oc zk)bysDJ-Vp4c(g}1(dG~#j<`9Z8(MY26C{xl#QW3bxXa;XFp53|J459%AS8SNVO^%YHbMiA{Outvi^F8AGC~+{u3%-kaj)XJI zT43oZ?)1wi{J+8H+Whj5d7q|dKRO*-_{&VauoX-0osF(d=R~o`z#*n(<$vbA@@LI^ zDz#T|@cbd!Y}{X2sD8g9QbyjXO~RIa)khiXp~_iGruv->13pRY9YM3!jI&f!9Q9|(^TzyqR%G0oF2Cc=XMf?C>0FJyL@+W+NYIlqM;)`u{<(|^ zRsEn}Ml7wO*gF7fXX@OT3$44uQXq$h406H7&8S`Lu`TC(ypBDaVr$S`E%x@x-B@i} z>@er=L(aFSFKK?^stW0!Wtvav*5ZJ&$G4g#*H@Q$d#GRuGsXSAQllU!z;|$P!K)`O zqG?uGH;7C{QJ2IC-mqrL$icVBDM{S+7Jn%E*A&Xh@T!x@$lMGp=z}zLa?5;vY*fwI zDm$-_p8hhWJdakHcm-RlLJT#2P>OLHc3y!q_A>@5z1HtEKMA^X)7?iq7=P)T#bZ9H z6DzvR*MU^NwBqxJ;|24^{(9S-u>D4t1~oKkUj>`JMM$XTCzx-TKR>Tu?dLJR`P>SLkU0|b!yY1-J=M}8A`lrjIErOqpJ7Jv(m3I`q1;G4LaW;Pf)mVE&v^nU4iHZE@=K26L1b=f46mIc2aPke18P(g zu)>}QAD_FpFYUQ1#nQi3gZ+|{%{?IVW`d3}^+cMJ;xO8kag)wTb{zI}x9(>&Szz%^ zD`fKRhRA0xiSI@AXVWr@C6Tj)jP8cM9th>Tt?D@id)PCHpU1rq;qhU1HkyJt&zxwU z;1#jk^RKRjh56j|U^LEaWn~<+5%t|*Sf)>i2O>WZL=Yq=^rl8lPTPfzK$wD>gHZAI zp?BK!kkiqN36A^i(4Ioxf>gFDR9^{QFX6{{?Y4AfU4YtIL0Ugos-8Ngg!LE=> zjkB{;OQlDgzC9PN*H<(DKA-4Elk~J&o)zw-?RxU2NI`_XS4Up*c^+VE2%l~08tgV} zMyCqT6U`S6b&%4Md>sa$9@3l#yhcD%Z9O8{rF>uh}i&k zErefDy7?%Z%*EtKY<<#)^r4?+NON(Sp@#9~fYzW;=-Rn=28DT*TDktCx%Qf(A6^WO zH1d~aKF2qb4pr=r{Nw!1_3QY3e3JV@dS{FDmCWF@cpesp6v?RWWvhAaC3>jsZbsb2 zdBjj{#tf{1P1klh86^3WMR#L`Lx16_?FtPHq|&U?^xtpo(@kdpU4sAitH!zo53zw6HV zGv05RfDx*q;VKWk1y8hWB)w+8M=74g;27;a6eWq0#1uSP*idd!91%JG;IyD>2z1w2 z%C$>W4bqn!Eh)mEbi3?KSVYz@N88@@cIL$2d%8PY3ZA}dt=5~*op|5YxcV7HmUHHb z6;WMYpUz3O>hpxYu`t<>9#fy%T0Mc-6N_LHQS7?4duIgK7oySa9xS}y;0@O8ur{an zXzF~FnfA6-c_=*3&2hUBr#vU%v|LDJ4Ht?%+B4L5z-bD_I%Z?^%tZ5b zBLl+Vbs?GpAW(c~I86vq>bQEfNw{i>9#3R_3%jtxO73I!w`*8CJhxDH?B|#*RZ`iy zbaT6|4Yl$GHd(l|uGf=)j;it;7v19?u!gYjq3f*BwP+1H?P(7&j$*mn0)uw>oEYAa zL`io+;HqtvX0~)8Q*%Pe+8U^l-KJbLDF@10G)r`q%|t!WRDc~` z9G;sOFcm!iipz3T{^xf4?L{`Hs4Qls>FZ@eZQe5U{3h~pH}>a&J75aXET4YR7?QL# zaiw^5uh6hR7Kwu=PKeJX8udm4QP&=Pl<^|B@2>o8VYVy4W~fM`+OZA>Mj;HxB$0(& zs6`t8wrwl)j8lsX7DE { + const canvas = within(canvasElement); + await expect(await canvas.findByText("Connection request withdrawn")).toBeVisible(); + await expect(canvas.queryByRole("button", { name: "Fix connection" })).not.toBeInTheDocument(); + }, +}; export const AiInlineRepair = aiRepair; export const AiInlineRepairNarrow: Story = { ...aiRepair, globals: { viewport: { value: "mobile1", isRotated: false } } }; export const AiRepairCancel: Story = { ...aiRepair, play: async context => { diff --git a/ui/storybook/stories/skills/project-repository-skills/README.md b/ui/storybook/stories/skills/project-repository-skills/README.md new file mode 100644 index 00000000000..c4d61d27ca1 --- /dev/null +++ b/ui/storybook/stories/skills/project-repository-skills/README.md @@ -0,0 +1,21 @@ +# Project repository skills + +Open **Agents and reusable capabilities → Skills and Skill Studio → Project repository skills** in Storybook. + +- **01 Components**: legacy adoption, progress, connection errors, private access, + automatic-update preference and manual refresh controls. +- **02 Journeys**: production project creation, Sources, library and routine pages + in the production app shell. Create a project with `acme/team-skills`, then + navigate to Skills → Sources. Legacy adoption starts before import. Background + update introduces skills without a click. Retry the failed refresh, pause updates, + inspect the private/shared source and try the mobile and light-theme variants. + Connection repair chooses an existing authorized account without changing skill + selections. Also inspect removed skills, invalid upstream content, two private + projects and the large-list story. + +All GitHub, import, scheduled update and routine execution responses are scoped +in-memory fixtures, reset between stories. They do not prove live synchronization, +credential refresh or model execution. Production acceptance evidence is recorded +in `doc/plans/2026-10-12-project-repository-skills.md`. + +Browser checks: `tests/storybook-visual/project-repository-skills.spec.ts`. diff --git a/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx b/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx new file mode 100644 index 00000000000..baf62b43be0 --- /dev/null +++ b/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { ProjectSkillRepositoryRow } from '@/components/skill-studio/ProjectSkillRepositoryRow'; +import { SkillSourceSyncControls } from '@/components/skill-studio/SkillSourceSyncControls'; +import { projectRepository } from '../../../support/projectRepositorySkills'; + +function Components({ state = 'available' }: { state?: 'available' | 'pending' | 'error' | 'private' | 'paused' | 'refreshing' }) { + const [imported, setImported] = useState(false), [automatic, setAutomatic] = useState(state !== 'paused'); + return

; +} +const meta = { id: 'skills-project-repository-skills-01-components', title: 'Agents and reusable capabilities/Skills and Skill Studio/Project repository skills/01 Components', component: Components, parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production Sources controls with fixture data cover empty, updating, paused, removed, invalid and private-project states. Provider access, persistence and runtime delivery require live acceptance."}, layout: 'fullscreen' } } satisfies Meta; +export default meta; +type Story = StoryObj; +export const Available: Story = {}; +export const Pending: Story = { args: { state: 'pending' } }; +export const Error: Story = { args: { state: 'error' } }; +export const Private: Story = { args: { state: 'private' } }; +export const Paused: Story = { args: { state: 'paused' } }; +export const Refreshing: Story = { args: { state: 'refreshing' } }; diff --git a/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx b/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx new file mode 100644 index 00000000000..55a47cdd47d --- /dev/null +++ b/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useQueryClient } from '@tanstack/react-query'; +import { Layout } from '@/components/Layout'; +import { SkillSources } from '@/pages/SkillSources'; +import { CompanySkills } from '@/pages/CompanySkills'; +import { Projects } from '@/pages/Projects'; +import { ProjectDetail } from '@/pages/ProjectDetail'; +import { RoutineDetail } from '@/pages/RoutineDetail'; +import { Routines } from '@/pages/Routines'; +import { IssueDetail } from '@/pages/IssueDetail'; +import { PluginLauncherProvider } from '@/plugins/launchers'; +import { Route, Routes, useNavigate } from '@/lib/router'; +import { installProjectSkillFixtures, ROUTINE_ID, type Scenario } from '../../../support/projectRepositorySkills'; + +function Journey({ scenario = 'ready' }: { scenario?: Scenario }) { + const client = useQueryClient(), navigate = useNavigate(); + const initial = useRef(navigate); + const [ready, setReady] = useState(false); + useEffect(() => { + const cleanup = installProjectSkillFixtures(scenario, client); + initial.current(scenario === 'new-project' ? '/projects' : scenario === 'attach-repository' ? '/projects/project-skills/configuration' : scenario === 'latest' ? `/routines/${ROUTINE_ID}` : '/skills/sources', { replace: true }); + setReady(true); return cleanup; + }, [scenario, client]); + if (!ready) return null; + return }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + ; +} +const meta = { id: 'skills-project-repository-skills-02-journeys', title: 'Agents and reusable capabilities/Skills and Skill Studio/Project repository skills/02 Journeys', component: Journey, tags: ['!autodocs'], + parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions", "projects/repositories", "routines/run-now", "routines/history"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production app shell and pages exercise project creation/attachment, legacy adoption, refresh/repair and routine revision journeys with in-memory GitHub and execution fixtures. Live authorization and GitHub-to-run delivery are verified separately."}, layout: 'fullscreen', docs: { description: { component: 'Production pages and app shell. Imports, GitHub responses, background refresh and routine executions are in-memory fixtures. Start with Create project or Legacy adoption; all actions remain interactive. Live integration is verified separately.' } } }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const CreateProject: Story = { name: '01 · Create a project', args: { scenario: 'new-project' } }; +export const LegacyAdoption: Story = { name: '02 · Import from an older project', args: { scenario: 'legacy' } }; +export const Synced: Story = { name: '03 · Automatically imported skills' }; +export const AutomaticUpdate: Story = { name: '04 · Background update and a new skill', args: { scenario: 'automatic' } }; +export const Refreshing: Story = { name: '05 · Refresh in progress', args: { scenario: 'refreshing' } }; +export const FailedRefresh: Story = { name: '06 · Retained skills and retry', args: { scenario: 'error' } }; +export const EmptyRepository: Story = { name: '07 · No skills yet', args: { scenario: 'empty' } }; +export const PrivateProject: Story = { name: '08 · Private project access', args: { scenario: 'private' } }; +export const SharedRepository: Story = { name: '09 · Access through either project', args: { scenario: 'shared' } }; +export const RoutineReference: Story = { name: '10 · Routine follows the installed skill', args: { scenario: 'latest' } }; +export const LongRepository: Story = { name: '11 · Long repository name', args: { scenario: 'long' } }; +export const Mobile: Story = { name: '12 · Mobile', globals: { viewport: { value: 'mobile1', isRotated: false } } }; +export const Light: Story = { name: '13 · Light theme', globals: { theme: 'light' } }; +export const PausedUpdates: Story = { name: '14 · Automatic updates paused', args: { scenario: 'paused' } }; +export const RemovedSkill: Story = { name: '15 · Removed skill retained', args: { scenario: 'removed' } }; +export const InvalidUpdate: Story = { name: '16 · Invalid upstream content', args: { scenario: 'validation' } }; +export const ConnectionRepair: Story = { name: '17 · Repair setup connection', args: { scenario: 'repair' } }; +export const TwoPrivateProjects: Story = { name: '18 · Two private projects', args: { scenario: 'shared-private' } }; +export const ManySkills: Story = { name: '19 · Large skill list', args: { scenario: 'many' } }; + +export const AttachRepository: Story = { name: '20 · Attach another repository', args: { scenario: 'attach-repository' } }; +export const RepairConflict: Story = { name: '21 · Repair during a background update', args: { scenario: 'repair-conflict' } }; diff --git a/ui/storybook/support/projectRepositorySkills.ts b/ui/storybook/support/projectRepositorySkills.ts new file mode 100644 index 00000000000..858945c4c74 --- /dev/null +++ b/ui/storybook/support/projectRepositorySkills.ts @@ -0,0 +1,217 @@ +import type { QueryClient } from '@tanstack/react-query'; +import type { ProjectSkillRepository, RoutineRunSummary, Issue } from '@paperclipai/shared'; +import { buildSkillMentionHref } from '@paperclipai/shared'; +import { skillSourcesApi } from '@/api/skillSources'; +import { companySkillsApi } from '@/api/companySkills'; +import { projectsApi } from '@/api/projects'; +import { routinesApi } from '@/api/routines'; +import { activityApi } from '@/api/activity'; +import { heartbeatsApi } from '@/api/heartbeats'; +import { emailApi } from '@/api/email'; +import { ApiError } from '@/api/client'; +import { issuesApi } from '@/api/issues'; +import { queryKeys } from '@/lib/queryKeys'; +import { baseRoutine } from '../fixtures/routineWebhooks'; +import { storybookProjects, storybookIssues } from '../fixtures/paperclipData'; +import { COMPANY_ID, SOURCE_ID, COMMIT, installFixtures, candidates, sourceFixture } from '../fixtures/githubSkillSources'; + +export type Scenario = 'new-project' | 'attach-repository' | 'legacy' | 'ready' | 'refreshing' | 'error' | 'empty' | 'private' | 'shared' | 'shared-private' | 'long' | 'many' | 'paused' | 'removed' | 'validation' | 'repair' | 'repair-conflict' | 'automatic' | 'latest'; +export const projectRepository: ProjectSkillRepository = { + projectId: 'project-skills', projectName: 'Acme platform', projectVisibility: 'open', + workspaceId: 'workspace-skills', repositoryUrl: 'https://github.com/acme/team-skills', trackingRef: 'main', + bindingId: null, sourceId: null, status: 'available', lastError: null, +}; +export const ROUTINE_ID = 'routine-project-skills'; +const NEW_COMMIT = 'b'.repeat(40); + +/** All mutations are scoped in-memory API fixtures. No provider calls or server writes. */ +export function installProjectSkillFixtures(scenario: Scenario, client: QueryClient) { + const cleanup = installFixtures(['new-project', 'legacy'].includes(scenario), false, { journey: true, repositories: 'multiple' }); + const baseSources = { ...skillSourcesApi }, baseSkills = { ...companySkillsApi }; + const originalActivity = { ...activityApi }, originalHeartbeats = { ...heartbeatsApi }; + const originalEmailThread = emailApi.thread; + const originalProjects = { ...projectsApi }, originalRoutines = { ...routinesApi }, originalIssues = { ...issuesApi }; + const timers: ReturnType[] = []; + let projectCreated = scenario !== 'new-project'; + const updatedSources = new Set(); + const sourcePreferences = new Map(); + let live = true, automaticUpdates = scenario !== 'paused', failed = ['error', 'repair', 'repair-conflict'].includes(scenario); + let sourceRevision = sourceFixture().revision, repairConflict = scenario === 'repair-conflict'; + const privateProject = scenario === 'private'; + let repository = { ...projectRepository, projectVisibility: privateProject ? 'private' as const : 'open' as const }; + if (!['new-project', 'legacy'].includes(scenario)) repository = { ...repository, sourceId: SOURCE_ID, bindingId: 'binding-skills', status: 'ready' }; + let additionalRepositories: ProjectSkillRepository[] = []; + let project = { ...storybookProjects[0]!, id: repository.projectId, urlKey: repository.projectId, name: repository.projectName, companyId: COMPANY_ID, + workspaces: storybookProjects[0]!.workspaces.slice(0, 1).map(workspace => ({ ...workspace, id: repository.workspaceId, projectId: repository.projectId, repoUrl: repository.repositoryUrl, repoRef: 'main', defaultRef: 'main', name: 'acme/team-skills', metadata: { ...workspace.metadata, githubRepositoryId: '123456' } })) }; + const workspaceTemplate = project.workspaces[0]!; + const invalidate = async () => { + await Promise.all([ + client.invalidateQueries({ queryKey: queryKeys.skillSources.all(COMPANY_ID) }), + client.invalidateQueries({ queryKey: queryKeys.companySkills.list(COMPANY_ID) }), + client.invalidateQueries({ queryKey: ['project-skill-repositories', COMPANY_ID] }), + client.invalidateQueries({ queryKey: queryKeys.routines.detail(ROUTINE_ID) }), + ]); + }; + const delay = (ms: number) => new Promise(resolve => timers.push(setTimeout(resolve, ms))); + const available = () => candidates.filter(candidate => !candidate.error && !candidate.path.startsWith('experimental/')).map(candidate => candidate.path); + function updateRepository(row: ProjectSkillRepository) { + if (row.workspaceId === projectRepository.workspaceId) repository = row; + else additionalRepositories = additionalRepositories.map(existing => existing.workspaceId === row.workspaceId ? row : existing); + } + async function adopt(row = repository) { + const bindingId = `binding-${row.workspaceId}`; + updateRepository({ ...row, status: 'pending', bindingId }); + await invalidate(); await delay(600); + if (!live) return; + const existing = (await baseSources.list(COMPANY_ID)).find(source => source.repositoryUrl === row.repositoryUrl && source.trackingRef === row.trackingRef); + const source = existing ?? (await baseSources.create(COMPANY_ID, { repositoryUrl: row.repositoryUrl, trackingRef: row.trackingRef, + commitSha: COMMIT, connectionId: 'github-storybook', packageMode: 'repository', selectedPaths: available(), excludedFolders: ['experimental'] })).source; + updateRepository({ ...row, bindingId, sourceId: source.id, status: 'ready' }); + await invalidate(); + } + async function refresh(_companyId = COMPANY_ID, requestedSourceId?: string) { + await delay(450); + if (failed) { failed = false; throw new Error('GitHub could not be reached. The installed skills were kept. Try again.'); } + const sourceId = requestedSourceId ?? (await baseSources.list(COMPANY_ID))[0]!.id; + const result = await baseSources.refresh(COMPANY_ID, sourceId); + await baseSources.select(COMPANY_ID, sourceId, { revision: result.source.revision, selectedPaths: available(), excludedFolders: ['experimental'] }); + updatedSources.add(sourceId); + return { ...result, source: (await skillSourcesApi.list(COMPANY_ID))[0]! }; + } + skillSourcesApi.list = async companyId => (await baseSources.list(companyId)).map(source => ({ ...source, + revision: Math.max(source.revision, sourceRevision), + automaticUpdates, importNewSkills: true, ...sourcePreferences.get(source.id), accessScope: 'projects', + syncState: scenario === 'refreshing' ? 'refreshing' : failed ? 'error' : automaticUpdates ? 'ready' : 'paused', + fullName: scenario === 'long' ? 'acme/platform-services-production-web-accessibility-improvements' : source.fullName, + lastScanCommit: updatedSources.has(source.id) ? NEW_COMMIT : source.lastScanCommit, + lastError: failed ? scenario.startsWith('repair') ? 'The person who connected this source no longer has company access. Choose another GitHub connection.' + : 'GitHub could not be reached. The installed skills were kept.' : null, + projects: ['shared', 'shared-private'].includes(scenario) ? [{ id: project.id, name: 'Private research', visibility: 'private', active: true }, + { id: 'project-open', name: 'Engineering', visibility: scenario === 'shared' ? 'open' : 'private', active: true }] + : [{ id: project.id, name: project.name, visibility: repository.projectVisibility, active: true }], + entries: scenario === 'empty' ? [] : scenario === 'many' ? Array.from({ length: 80 }, (_, index) => ({ + ...source.entries[0]!, id: 'entry-' + index, name: 'Project workflow ' + (index + 1), skillId: 'skill-' + index, + })) : source.entries.map((entry, index) => index === 1 && scenario === 'removed' ? { ...entry, present: false } + : index === 1 && scenario === 'validation' ? { ...entry, error: 'SKILL.md contains an invalid package reference. The installed version was kept.' } : entry), + })); + skillSourcesApi.projectRepositories = async () => !projectCreated || project.workspaces.length === 0 ? [] : structuredClone([repository, ...additionalRepositories]); + skillSourcesApi.adoptProject = async (_companyId, workspaceId) => { + const row = [repository, ...additionalRepositories].find(row => row.workspaceId === workspaceId)!; + await adopt(row); return { bindingId: `binding-${workspaceId}` }; + }; + skillSourcesApi.refresh = refresh; + skillSourcesApi.settings = async (_companyId, id, input) => { + if (input.connectionId !== undefined && repairConflict) { + repairConflict = false; sourceRevision++; + throw new ApiError('The source was updated while choosing a connection.', 409, null); + } + if (input.revision !== undefined && input.revision < sourceRevision) { + throw new ApiError('The source revision changed.', 409, null); + } + sourcePreferences.set(id, { ...sourcePreferences.get(id), + ...(input.automaticUpdates === undefined ? {} : { automaticUpdates: input.automaticUpdates }), + ...(input.importNewSkills === undefined ? {} : { importNewSkills: input.importNewSkills }), + ...(input.connectionId === undefined ? {} : { connectionId: input.connectionId }), + }); + if (input.connectionId !== undefined) failed = false; + return (await skillSourcesApi.list(COMPANY_ID)).find(source => source.id === id)!; + }; + companySkillsApi.detail = async (companyId, id) => { + const skill = await baseSkills.detail(companyId, id); + const hasUpdate = updatedSources.has(String(skill.metadata?.skillSourceId)); + return { ...skill, accessSourceId: String(skill.metadata?.skillSourceId), sourceRef: hasUpdate ? NEW_COMMIT : skill.sourceRef, + markdown: hasUpdate ? `${skill.markdown}\nCheck the project access before publishing results.\n` : skill.markdown, + currentVersionId: hasUpdate ? `${skill.currentVersionId}-updated` : skill.currentVersionId }; + }; + projectsApi.repositoryOptions = baseSources.repositories; + projectsApi.get = async () => structuredClone(project); + projectsApi.list = async () => !projectCreated ? [] : [structuredClone(project)]; + projectsApi.setRepositories = async (_id, repositoryIds) => { + const options = await projectsApi.repositoryOptions(COMPANY_ID); + const existingIds = new Set(project.workspaces.map(workspace => workspace.metadata?.githubRepositoryId)); + const template = workspaceTemplate; + const workspaces = repositoryIds.map((id, index) => { + const selected = options.repositories.find(option => option.id === id)!; + return { ...template, id: index === 0 ? projectRepository.workspaceId : `workspace-${id}`, name: selected.fullName, + repoUrl: selected.url, metadata: { ...template.metadata, githubRepositoryId: id }, isPrimary: index === 0 }; + }); + project = { ...project, workspaces, primaryWorkspace: workspaces[0] ?? null }; + for (const workspace of workspaces) { + if (existingIds.has(workspace.metadata.githubRepositoryId)) continue; + const row: ProjectSkillRepository = { ...projectRepository, projectName: project.name, workspaceId: workspace.id, + repositoryUrl: workspace.repoUrl, status: 'pending', bindingId: `binding-${workspace.id}` }; + if (workspace.id === repository.workspaceId) repository = row; + else additionalRepositories.push(row); + void adopt(row); + } + return structuredClone(project); + }; + projectsApi.create = async (_companyId, input) => { + projectCreated = true; + project = { ...project, name: String(input.name), workspaces: [], primaryWorkspace: null }; + repository = { ...repository, projectName: project.name }; + return projectsApi.setRepositories(project.id, (input.repositoryIds ?? []) as string[]); + }; + const skillId = sourceFixture().entries[1]!.skillId!; + let routine = { ...baseRoutine, id: ROUTINE_ID, title: 'Review project changes', projectId: project.id, + project: { id: project.id, name: project.name, description: null, status: 'in_progress' }, + description: `Use [/code-review](${buildSkillMentionHref(skillId, 'code-review')}) to review the project changes and report your findings.`, triggers: [], recentRuns: [] as RoutineRunSummary[] }; + let executionIssues: Issue[] = [storybookIssues[0]!].map(issue => ({ ...issue, id: 'project-review-before', projectId: project.id, + originKind: 'routine_execution' as const, originId: ROUTINE_ID, title: 'Review project changes', status: 'done' as const, + executionWorkspaceId: null, projectWorkspaceId: null, currentExecutionWorkspace: null, labels: [], + description: 'Completed using the installed code review checklist.' })); + routinesApi.get = async () => structuredClone(routine); + routinesApi.list = async () => [structuredClone({ ...routine, lastRun: routine.recentRuns[0] ?? null })]; + routinesApi.listRuns = async () => structuredClone(routine.recentRuns); + routinesApi.activity = async () => []; + routinesApi.listRevisions = async () => []; + routinesApi.update = async (_id, input) => (routine = { ...routine, ...input }); + routinesApi.run = async () => { + const now = new Date(); + const executionId = `project-review-${routine.recentRuns.length + 1}`; + const resolvedSkill = await companySkillsApi.detail(COMPANY_ID, skillId); + const run: RoutineRunSummary = { id: `review-run-${routine.recentRuns.length}`, companyId: COMPANY_ID, routineId: ROUTINE_ID, + triggerId: null, source: 'manual', status: 'completed', idempotencyKey: null, triggerPayload: null, dispatchFingerprint: null, + linkedIssueId: executionId, coalescedIntoRunId: null, failureReason: null, triggeredAt: now, completedAt: now, createdAt: now, updatedAt: now, + trigger: null, linkedIssue: { id: executionId, identifier: `PAP-${124 + routine.recentRuns.length}`, title: 'Review project changes', status: 'done', priority: 'medium', updatedAt: now } }; + routine = { ...routine, recentRuns: [run, ...routine.recentRuns] }; + executionIssues = [{ ...executionIssues[0]!, id: executionId, identifier: run.linkedIssue!.identifier, + description: `Completed using code review version ${resolvedSkill.currentVersionId} at commit ${resolvedSkill.sourceRef}.`, + }, ...executionIssues]; + return run; + }; + const isExecution = (id: string) => executionIssues.some(issue => issue.id === id || issue.identifier === id); + emailApi.thread = async (companyId, id) => isExecution(id) ? null : originalEmailThread(companyId, id); + activityApi.forIssue = async id => isExecution(id) ? [] : originalActivity.forIssue(id); + activityApi.runsForIssue = async id => isExecution(id) ? [] : originalActivity.runsForIssue(id); + heartbeatsApi.liveRunsForIssue = async id => isExecution(id) ? [] : originalHeartbeats.liveRunsForIssue(id); + heartbeatsApi.activeRunForIssue = async id => isExecution(id) ? null : originalHeartbeats.activeRunForIssue(id); + heartbeatsApi.executionForIssue = async id => isExecution(id) ? null : originalHeartbeats.executionForIssue(id); + issuesApi.listComments = async (id, options) => isExecution(id) ? [] : originalIssues.listComments(id, options); + issuesApi.listInteractions = async id => isExecution(id) ? [] : originalIssues.listInteractions(id); + issuesApi.listAttachments = async id => isExecution(id) ? [] : originalIssues.listAttachments(id); + issuesApi.listApprovals = async id => isExecution(id) ? [] : originalIssues.listApprovals(id); + issuesApi.listWorkProducts = async (id, options) => isExecution(id) ? [] : originalIssues.listWorkProducts(id, options); + issuesApi.listFeedbackVotes = async id => isExecution(id) ? [] : originalIssues.listFeedbackVotes(id); + issuesApi.listTreeHolds = async (id, options) => isExecution(id) ? [] : originalIssues.listTreeHolds(id, options); + issuesApi.listAcceptedPlanDecompositions = async id => isExecution(id) ? [] : originalIssues.listAcceptedPlanDecompositions(id); + issuesApi.getQueuedComments = async id => isExecution(id) ? { + issueId: id, queueId: null, state: null, targetRunId: null, revision: 'empty', protocol: 'legacy', steeringDisposition: 'unsupported', entries: [], + } : originalIssues.getQueuedComments(id); + issuesApi.getTreeControlState = async id => isExecution(id) ? { activePauseHold: null } : originalIssues.getTreeControlState(id); + const originalList = originalIssues.list, originalCompact = originalIssues.listCompact; + issuesApi.get = async (id, options) => { + const issue = executionIssues.find(candidate => candidate.id === id || candidate.identifier === id); + return issue ? structuredClone(issue) : originalIssues.get(id, options); + }; + issuesApi.list = async (companyId, filters) => filters?.originId === ROUTINE_ID ? structuredClone(executionIssues) : originalList(companyId, filters); + issuesApi.listCompact = async (companyId, filters, options) => filters?.originId === ROUTINE_ID ? structuredClone(executionIssues.map(issue => ({ ...issue, activeRecoveryAction: null, successfulRunHandoff: null }))) : originalCompact(companyId, filters, options); + if (scenario === 'automatic') timers.push(setTimeout(() => { if (live) void refresh().then(invalidate); }, 1800)); + return () => { + live = false; timers.forEach(clearTimeout); + Object.assign(activityApi, originalActivity); Object.assign(heartbeatsApi, originalHeartbeats); + emailApi.thread = originalEmailThread; + Object.assign(projectsApi, originalProjects); Object.assign(routinesApi, originalRoutines); Object.assign(issuesApi, originalIssues); + cleanup(); + }; +}