3. Task Store

The Task Store lets Agora users browse, import, and publish Task and Trigger definitions shared across GyroTools customers. See Task Store for the end-user guide. This page describes the architecture behind it.

3.1. Architecture

The feature spans three tiers:

  1. License server (gt_task_store app) — the actual shared data store, hosted centrally by GyroTools.

  2. Agora Django backend (agora.task_store) — proxies between the Agora frontend and the license server, and stores each user’s link to their GyroTools portal account.

  3. Agora Angular frontend — the Task Store / Trigger Store screens and the account-linking UI.

3.2. License Server

The license server’s gt_task_store app owns the TaskStoreEntry model (gt_task_store/models.py):

Field

Description

owner

FK to the portal User who published the entry.

type

task or trigger — which kind of bundle this entry holds. The store is otherwise generic: entries of both types share the same model and endpoints, dispatched by this field rather than by separate task/trigger endpoints.

name / description / long_description / image

Display metadata shown in the store list and detail views.

tasks

Opaque JSONField holding the raw bundle. The license server never interprets its contents — it stores and serves it back verbatim.

tags

M2M to TaskStoreTag, used for search and filtering.

download_count

Incremented on every download call.

TaskStoreEntryViewSet (gt_task_store/rest.py) is a full ModelViewSet with a split authentication scheme:

  • Reads (list, retrieve, download) use CalKeyAuthentication — the install’s shared CAL/license key, the same key every Agora installation already sends on /check, /renew, /info. No per-user login is required to browse; only a currently-licensed Agora install.

  • Writes (create, update, destroy) use TokenAuthentication with IsOwnerOrReadOnly — a real per-user identity is required, obtained via account linking (see below). IsOwnerOrReadOnly compares obj.owner_id against the requesting user.

The download action returns {"type": entry.type, "tasks" | "triggers": entry.tasks} — the bundle key matches whichever key Agora’s own local export/import already uses for that type, so a downloaded bundle is byte-for-byte the same shape as a local export.

AccountLinkView (POST /link/) exchanges a GyroTools portal email/password for the user’s personal API key: it calls Django’s authenticate() against the portal’s user database, then get_api_key_for_user() (the same helper backing the portal’s own “regenerate my API key” flow) to obtain a rest_framework.authtoken token. It is throttled per-IP (AccountLinkThrottle) since it accepts raw credentials.

Note

Anyone who successfully links their portal account can publish — there is no additional role or permission check on the license server beyond IsOwnerOrReadOnly. This is an intentional simplification for the current release and may need revisiting if it becomes a problem in practice.

Admin: the gt_admin Task Store admin list now has a per-row Delete action (gt_admin/tables.py TaskStoreEntryTable, gt_admin/views.py TaskStoreEntryDelete, URL admin-task-store-entry-delete). Previously entries could only be created and edited from the admin, not removed.

3.3. Agora Backend

agora.task_store (srv/agora/task_store/) proxies between the Agora frontend and the license server, and owns one model of its own:

TaskStoreAccountLink (models.py) — a one-to-one link from a local Agora User to their GyroTools portal identity:

Field

Description

user

OneToOneField to the Agora User.

linked_email

The portal account’s email, shown in the UI as the linked identity.

api_key

The portal account’s personal API key, stored encrypted (django_cryptography.fields.encrypt). Agora never stores the portal password — only the token returned once by the license server’s /link/ endpoint.

TaskStoreService (services.py) wraps all calls to the license server:

  • search_entries / get_entry / download_bundle — reads, authenticated with the install’s own license key (license_service.get_agora_license().key). Raises TaskStoreServiceException if there is no valid license.

  • link_account / unlink_account / get_account_link — manage the local TaskStoreAccountLink for a given Django user.

  • create_entry / update_entry / delete_entry — owner-write actions. Each calls _require_link() first and raises TaskStoreServiceException if the current user has no linked account, authenticating to the license server with the linked account’s stored API key.

  • import_bundle — a purely local operation (no license-server call): takes an already-downloaded bundle and its type field to dispatch to _import_task_bundle or _import_trigger_bundle, reusing the existing task/trigger creation serializers (TaskDefinitionSerializer / TaskDefinitionYamlSerializer for tasks, TriggerImportSerializer for triggers) — the same code paths used by local .json import.

api/views.py exposes this over HTTP, all requiring an authenticated Agora session (IsAuthenticated):

  • TaskStoreEntryListView — GET (search) / POST (create).

  • TaskStoreEntryDetailView — GET / PATCH / DELETE on one entry.

  • TaskStoreEntryDownloadView — GET the raw bundle.

  • TaskStoreEntryImportView — POST to import a downloaded bundle into a project. Checks can_manage_tasks / can_manage_triggers on the target project twice: once up front (rejecting a user with neither permission before any network call), and again after the bundle’s actual type is known from the license server, in case the user has only one of the two permissions.

  • TaskStoreAccountView — GET link status / POST link / DELETE unlink.

See REST API for the full endpoint table and request/response examples.

3.4. Agora Frontend

Two structurally parallel feature modules, both under project/modules/settings/modules/:

  • task-store/ and trigger-store/ — each has services/ (*-store.service.ts for entry CRUD/search/import, *-store-publish.service.ts for opening the publish modal), store/ (NgRx actions/effects/reducers/selectors for search/load/create/update/delete), components/ (*-store-list, *-store-detail, *-store-publish-modal), and containers/ (list/detail containers that connect them to the store).

Account linking lives in its own core module, core/task-store-account/, mirroring the existing core/auth module pattern: a small NgRx feature (taskStoreAccount), a TaskStoreAccountService, imported by both store modules and by the profile module. Keeping it outside either store module lets the Task Store and Trigger Store share one account link without depending on each other.

Entry points in the UI:

  • Browse — a store icon button in the Task/Trigger toolbar, navigating to the store list screen.

  • Publish — a button inside the store list screen itself, opening the publish modal. If the account isn’t linked yet, the modal shows the inline link form first; once linked, it shows the picker + metadata form. Editing an owned entry reopens the same modal pre-filled, skipping the picker.

  • Profile — a standalone Task Store tab (ui/src/app/profile/task-store-account/) next to API Key/Password/etc., for linking or unlinking outside of the publish flow.

Ownership in the UI is determined by comparing the entry’s owner_email against the current user’s linked email — the Edit/Delete buttons on the detail page are shown only when they match.