Microeden Agry SDK (ENG)

1. What Agry Dev Mode is

Agry Dev Mode is the development and testing environment for extensions that run inside Agry. A plugin is loaded by the host application, but third-party code executes in an isolated runtime. The isolation boundary is part of the security model: a plugin receives explicit bridge methods instead of direct access to the page, browser storage, the host DOM, or the host authentication session.

Dev Mode is available only to an authenticated developer account:

  • a persistent Microeden session is required;
  • the developer token must match the token supplied to Dev Mode;
  • the account must have the server-side dev label.

Being a Pro user alone does not grant access to Dev Mode or to publication in the Store.

2. Runtime model and lifecycle

An isolated plugin normally runs as an asynchronous JavaScript module or an immediately invoked asynchronous function:

(async function () {
  Agry.log("Plugin started", "info");
  // Register UI and event handlers here.
})();

The worker facade is exposed as Agry. A second facade named MyMicroeden exposes account and device services. Both are proxies over a validated RPC bridge.

Recommended lifecycle:

  1. Register widgets, ribbon buttons, windows, and event listeners.
  2. Read only the data needed for the current view.
  3. Keep local state in plugin storage when it must survive a reload.
  4. Register Agry.onStop for cleanup.
  5. Remove timers, handlers, markers, and windows when the plugin stops.
Agry.onStop(function () {
  Agry.time.clearInterval(refreshTimer);
  Agry.events.remove("activity.created", onActivity);
});

Callbacks are registered through the bridge and must remain small and serializable. Do not retain host DOM objects or attempt to call browser globals from the plugin.

3. Minimal plugin example

(async function () {
  var fields = await Agry.fields.list();
  await Agry.addWidget(
    "weather-widget",
    "Field weather",
    "☀️",
    "<p id=\"weather-widget-content\">Fields available: " + fields.length + "</p>"
  );

  // Widgets do not return a setHTML handle. Update their own content by ID.
  await Agry.dom.setHTML(
    "weather-widget-content",
    "<p>Fields available: " + fields.length + "</p>"
  );

  Agry.onMapClick(function (point) {
    Agry.log("Map click: " + point.lat + ", " + point.lng, "info");
  });
})();

Use the host DOM helper methods for plugin-owned UI. They return handles scoped to the plugin; a plugin cannot use them to select arbitrary application nodes.

4. Agry API reference

The following methods are available in the isolated third-party runtime. Methods return promises unless the description says that the method registers a callback.

The current bridge uses positional arguments for map, widget, window, and ribbon APIs. It does not accept the object-shaped options used by some older prototypes. DOM setters receive a plugin-owned element ID; they do not receive a browser DOM node.

4.1 Runtime information and logging

  • Agry.version - runtime API version string. The sandbox currently reports version 3.3 (sandbox).
  • Agry.log(message, type) - writes a diagnostic message. Typical types are info, success, warn, and error.
  • Agry.toast(message, type) - displays a short host toast.
  • Agry.onStop(callback) - registers a cleanup callback invoked when the plugin is stopped.

4.2 Fields

  • Agry.fields.list() - returns the available fields/project areas visible to the current account.
  • Agry.fields.getDiary(fieldName) - returns diary entries for a field.
  • Agry.fields.getTasks(fieldName) - returns scheduled and completed activities for a field.
  • Agry.fields.getFieldIcon(fieldName) - returns the field icon/emoji used by Agry.
  • Agry.getFields() - compatibility helper returning fields.
  • Agry.getNearestField(latitude, longitude) - finds the nearest visible field when location access is available.

Returned records are data snapshots. Treat fields such as names, identifiers, dates, activities, elements, weather, and collaborator assignments as optional because older projects can lack newer properties.

4.3 Diary and activities

Diary

Agry.diary.addEntry(fieldName, noteText, photoBase64) creates a diary entry. The photo argument is optional and should be a base64 data URL only when a photo is actually attached.

await Agry.diary.addEntry(
  "North field",
  "Observed good growth after irrigation.",
  null
);

Scheduled activities

  • Agry.tasks.schedule(fieldName, activityType, dateISO, time) creates a scheduled activity. time may be null for an all-day activity.
  • Agry.tasks.reschedule(fieldName, activityType, originalDateISO, newDateISO) moves an activity to a new date.
  • There is no public isolated tasks list method; read activities with Agry.fields.getTasks(fieldName).

Use ISO dates (YYYY-MM-DD) and, when present, a 24-hour time (HH:mm). Do not infer a local timezone by string concatenation; use the date helpers or the host values returned in activity records.

When an activity is assigned to a collaborator, preserve the assignment identifier and display name returned by the host. Do not use an email address as a visual label unless the host has no display name.

4.4 Warehouse

  • Agry.warehouse.getInventory() - returns inventory available to the current project/account.
  • Agry.warehouse.addItem(item) - adds or updates an inventory item.
  • Agry.warehouse.removeItem(itemId) - removes an item.
  • Agry.warehouse.save() - persists pending warehouse changes.

Warehouse data is capability-gated. A plugin should show an explicit empty state when access is denied, rather than assuming the warehouse is empty.

4.5 Network access

  • Agry.fetch(url, options) - validated network request for an external HTTPS service.
  • Agry.httpGet(url) - convenience GET request.
  • Agry.httpPost(url, body) - convenience JSON POST request.

Network requests pass through the sandbox firewall. See the Network and Sandbox sections below. The plugin cannot use the browser's native fetch, XHR, WebSocket, EventSource, or import APIs.

var response = await Agry.fetch("https://api.example.com/status", {
  method: "GET",
  headers: { "Accept": "application/json" }
});
var status = await response.json();

Do not send credentials, session cookies, CSR tokens, or other secrets. A domain may require explicit consent when the request exports sensitive project data.

4.6 Map

  • Agry.map.setView(latitude, longitude, zoom) - changes the map view.
  • Agry.map.drawShape(type, coordinates, options) - draws a plugin-owned shape.
  • Agry.map.clearLayers() - removes plugin-owned map layers.
  • Agry.map.getUserPosition() - requests the current position when allowed.
  • Agry.addMarker(latitude, longitude, popupContent, iconSource, alertLevel) - adds a plugin-owned marker.
  • Agry.removeMarker(markerHandle) - removes a plugin-owned marker returned by Agry.addMarker.
  • Agry.clearAllMarkers() - removes the plugin's markers.
  • Agry.onMapClick(callback) - registers a map click callback.

Example marker:

var marker = await Agry.addMarker(
  45.123,
  11.456,
  "<strong>Hive observation</strong>",
  "🐝",
  "normal"
);

Markers and layers are owned by the plugin. Always remove them during cleanup if they are not meant to remain after the plugin closes.

await Agry.map.drawShape(
  "circle",
  [45.123, 11.456],
  { radius: 100, color: "#89ba54", fillOpacity: 0.2 }
);

4.7 Ribbon buttons, widgets, and windows

  • Agry.addRibbonButton(label, icon, onClickFunc) - adds a button to the Agry ribbon.
  • Agry.addWidget(widgetId, title, icon, contentHTML) - creates a dashboard widget. A repeated call with the same ID is a no-op; it returns a success value, not a widget handle.
  • Agry.createWindow(windowId, title, contentHTML, width, height) - opens a plugin window/panel.
  • Agry.onWindowClose(windowId, callback) - registers a window-close callback.
  • Agry.drawRiskLink(startCoord, endCoord, level) - draws a risk/relationship link supported by the Agry visual layer.

Use a stable identifier, a short title, an emoji or approved icon, and plugin-owned content. Window and widget calls do not return mutable UI handles; update plugin-owned content through the scoped DOM bridge.

await Agry.createWindow(
  "my-plugin-panel",
  "Field report",
  "<p id=\"report-content\">Report ready.</p>",
  560,
  "auto"
);

await Agry.dom.setHTML("report-content", "<p>Updated report.</p>");

4.8 UI dialogs

  • Agry.ui.confirm(message, callback)
  • Agry.ui.prompt(message, defaultValue, callback)

These methods use host dialogs. The result is supplied to the callback; they do not guarantee an awaitable return value.

Agry.ui.confirm("Delete this plugin record?", function (confirmed) {
  if (confirmed) {
    Agry.log("Confirmed", "warning");
  }
});

Never rely on browser window.confirm, window.prompt, or alert; those globals are unavailable in the isolated runtime.

4.9 DOM and styles

The scoped DOM bridge provides:

  • Agry.dom.getElementById(id)
  • Agry.dom.createElement(tagName)
  • Agry.dom.querySelector(selector)
  • Agry.dom.removeElement(id)
  • Agry.dom.getValue(id)
  • Agry.dom.setValue(id, value)
  • Agry.dom.setHTML(id, html)
  • Agry.dom.setStyles(id, styles)
  • Agry.dom.setAttributes(id, attributes)
  • Agry.styles.set(styleId, cssText)

The first three methods return handles scoped to the plugin; those handles expose the compatibility helpers css, attr, html, and val. The explicit DOM setter/remover methods above receive the element ID string. Keep selectors and IDs inside the plugin namespace. HTML inserted by a plugin must be escaped when it contains user or project data.

4.10 Events

  • Agry.events.add(eventName, callback)
  • Agry.events.remove(eventName, callback)
  • Agry.events.getTargetId()

Event names are host-defined and may be extended over time. The callback should accept one plain serializable payload and return quickly.

4.11 Storage and files

Plugin storage is isolated from the site's local storage:

  • Agry.storage.set(key, value)
  • Agry.storage.get(key)
  • Agry.storage.clear() - clears the current plugin's stored keys.
  • Agry.storage.getAllKeys()

For user downloads:

  • Agry.file.download(filename, data, mimeType) - starts a download in the host UI.

Do not attempt localStorage, sessionStorage, IndexedDB, Cache Storage, or direct filesystem access. They are deliberately unavailable.

4.12 Date, time, and utilities

Date helpers:

  • Agry.date.now()
  • Agry.date.getHours(date)
  • Agry.date.getMinutes(date)
  • Agry.date.getSeconds(date)
  • Agry.date.format(date, withTime)
  • Agry.date.addDays(date, days)
  • Agry.date.diffDays(firstDate, secondDate)
  • Agry.date.toISO(date)

Timers:

  • Agry.time.setTimeout(callback, delay)
  • Agry.time.setInterval(callback, delay)
  • Agry.time.clearTimeout(id)
  • Agry.time.clearInterval(id)

Utilities:

  • Agry.utils.getDistance(a, b) - distance helper;
  • Agry.utils.formatArea(area) - area formatting helper.

The full host contains additional utility helpers, but the isolated facade intentionally exposes only the safe subset above.

4.13 Database snapshots

Agry.DB contains read-only snapshots used by supported UI features:

  • Agry.DB.areaIcons
  • Agry.DB.categories
  • Agry.DB.wildlife
  • Agry.DB.crop
  • Agry.DB.alerts
  • Agry.DB.presets
  • Agry.DB.schemas

The snapshot can be absent or incomplete in an older project. Never mutate it and never treat it as a write API.

5. MyMicroeden SDK bridge

The isolated runtime exposes a limited MyMicroeden facade:

var profile = await MyMicroeden.account.getProfile();
var devices = await MyMicroeden.devices.list();

Account

  • MyMicroeden.account.getProfile() - profile information such as name and roles, subject to consent.
  • MyMicroeden.account.getPreferences() - plugin-isolated preferences.
  • MyMicroeden.account.setPreference(key, value) - saves a plugin preference.
  • MyMicroeden.account.deletePreference(key) - deletes a plugin preference.

Devices

  • MyMicroeden.devices.list() - devices visible to the current account.
  • MyMicroeden.devices.getHistory(deviceId, options) - limited telemetry/history.
  • MyMicroeden.devices.getCategories() - device categories.

No other site SDK namespace should be assumed. A plugin must not call internal PHP endpoints directly to bypass the bridge or capability checks.

Sensitive APIs are guarded by the Plugin Capability Gatekeeper. The base scopes currently include:

Capability Purpose
account-profile-read Read account name, email, ID, and roles
preferences-read Read plugin-isolated preferences
preferences-write Write plugin-isolated preferences
devices-read Read devices and limited telemetry
project-data-read Read fields, activities, and geometries
diary-write Create diary entries
tasks-write Create or edit activities
warehouse-read Read warehouse data
warehouse-write Change warehouse data
location-read Read the user's position
plugin-storage-write Use isolated plugin storage
file-download Start a file download
sensitive-data-export:hostname Export sensitive data to a specific external host

Consent is stored per plugin and account until the user revokes it. Dev Mode may use an ephemeral choice while testing, but a published plugin must behave correctly when consent is denied or later revoked.

Capability principles:

  1. Request the smallest capability set that implements the feature.
  2. Explain why a capability is needed in the plugin UI or metadata.
  3. Handle a denied capability as a normal state.
  4. Do not encode secrets in source, metadata, or storage.
  5. Do not use a network request as a way to avoid a capability prompt.

7. Network policy

All external requests are validated before they leave the runtime.

  • HTTPS is required.
  • URL length is limited to 2048 characters.
  • URLs with a username or password are rejected.
  • Same-origin URLs and microeden.io or its subdomains are rejected by the plugin network bridge.
  • Localhost, local/internal/LAN/home/corp names, private/link-local/reserved/multicast addresses, and IPv6 literals are blocked.
  • Firewall-blacklisted domains are blocked.
  • Methods are limited to GET, HEAD, POST, PUT, PATCH, and DELETE.
  • GET and HEAD cannot have a body.
  • Request bodies must be strings and are limited to 32 KiB.
  • At most 20 request headers are accepted and each header is limited to 2048 characters.
  • Cookie, host, origin, referer, proxy authorization, CSR-token, and CSRF-token headers are blocked.
  • The default request budget is 20 requests per execution.
  • Each request has an eight-second timeout.
  • Response bodies are limited to 5 MiB.
  • Credentials are omitted, redirects fail, caching is disabled, and the referrer is not sent.

Use a server-side integration for credentials and rate limiting. Never place an API key in a plugin distributed through the Store.

8. Sandbox limits (informative)

This section is guidance for third-party developers, not a promise that limits will never change. The implementation currently uses a sandboxed iframe and a worker/RPC bridge. Native browser globals and direct host objects are intentionally unavailable.

Current validation limits include:

  • maximum RPC arguments: 30;
  • maximum in-flight RPC calls: 20;
  • maximum string argument: 16,384 characters;
  • maximum nested object depth: 7;
  • maximum array length: 500;
  • maximum object keys: 120;
  • object keys must match [A-Za-z0-9_.-]{1,96};
  • proto, prototype, and constructor keys are rejected;
  • cyclic objects and non-plain objects are rejected;
  • maximum callback registrations: 96;
  • serialized request budget: 32 KiB;
  • ordinary isolated response budget: 5 MiB;
  • MyMicroeden SDK response budget: 256 KiB.

The runtime additionally blocks attempts to reference or invoke:

  • window, document, globalThis, self, navigator, location, and history;
  • localStorage, sessionStorage, indexedDB, and caches;
  • XMLHttpRequest, WebSocket, EventSource, importScripts, Worker, SharedWorker, BroadcastChannel, and MessageChannel;
  • WebAssembly, eval, Function, dynamic import, and native fetch;
  • constructor, prototype, and postMessage tricks.

These checks are defence in depth. A plugin must still validate all input, escape rendered text, and protect any backend it owns.

9. Plugin source and package expectations

A Store plugin is identified by a stable plugin ID and metadata. A typical development source has a JavaScript entry point and a manifest in the plugin directory. The exact Store package schema is validated by the current catalog; files in the legacy my/agry/plugins/index.json are not sufficient for publication.

Recommended metadata:

  • stable ID;
  • human-readable name;
  • semantic version;
  • category;
  • concise description;
  • icon or emoji;
  • up to three screenshots;
  • entry source;
  • requested capabilities;
  • privacy and external-service disclosure.

Keep the entry source below the Developer Console source limit and split large static content into data or host-managed resources where possible.

10. Developer Console API

The endpoint my/developer_plugins_api.php is for the authenticated owner of the Developer Console. It is not a public runtime API. Requests require:

  • a persistent authenticated session;
  • the dev account label;
  • a valid MICROEDEN-CSR-TOKEN header;
  • same-site/referer checks as enforced by the endpoint.

Supported actions:

Action Description
list List the developer's plugins and statuses
quota Read source/storage quota
open_source Read the current source
autosave_source Save an autosave draft
save_draft Save a normal draft
submit_review Submit a plugin for review
update_metadata Update name, version, category, description, icon, and screenshots
delete Delete a developer plugin

Current server-side metadata limits:

  • name: required, up to 120 characters;
  • version: required, up to 30 characters;
  • category: required, up to 80 characters;
  • description: required, up to 2,000 characters;
  • icon: up to 512 characters;
  • screenshots: up to 3 URLs, each up to 1,500 characters;
  • source code: up to 45,000 characters.

Typical review statuses are draft, pending, review, testing, approved, published, and rejected. Pending/review/testing records are locked against ordinary editing. Editing an approved, published, or rejected plugin creates a new draft revision.

The server performs a static security scan. It flags access to browser globals, direct network constructors, dynamic code execution, dynamic imports, native fetch, prototype/constructor tricks, and cross-window messaging. A scan warning is not a substitute for manual review.

Plugin source is encrypted server-side with AES-256-GCM (enc:v2) and may be compressed before storage. Encryption protects stored source; it does not make unsafe plugin logic safe at runtime.

11. Store loading and execution modes

The current Store catalog is loaded from the authenticated cloud catalog. Catalog metadata and plugin source are separate operations. If a catalog entry has no migration source, the host reports a migration/source error; a plugin author should verify that the cloud source is published and that the stable ID is unchanged.

Third-party plugins execute in isolated mode. Only a small, explicitly trusted legacy integration uses the legacy host mode. A plugin must never depend on legacy access, internal globals, or DOM selectors outside its own scope.

12. Patterns for reliable plugins

Read and render

Read the minimum data, normalize optional fields, escape user text, and render deliberate loading, empty, error, and success states.

function escapeText(value) {
  return String(value == null ? "" : value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

Refresh

Use Agry.time.setInterval only when the feature needs polling, keep the interval conservative, and clear it in Agry.onStop. Avoid refreshing while a user is editing a form.

Errors

Catch bridge and network failures, log technical details with Agry.log, and show a short actionable Agry.toast. Never display raw server responses that could contain secrets.

Idempotency

Use stable IDs for widgets, windows, markers, and stored records. Before creating a duplicate, read the current plugin state; repeated widget/window calls with an existing ID are handled by the host rather than creating an unbounded set of objects.

Dates

Store dates in ISO form and display them with host date helpers. Respect the timezone represented by returned activity data.

Collaborators and projects

Use the display name supplied by Agry for collaborator assignments. Apply the host visibility rules: a collaborator sees only projects and activities shared with them, while the project owner may see the full project.

13. Troubleshooting

Sandbox argument too long

Reduce the size of a single RPC argument. Do not pass an entire project, image, or source file to one method. Store or upload data in bounded chunks, use a URL/reference where appropriate, and keep request bodies under the limits above.

widget.setHTML is not a function

Agry.addWidget(...) returns a success value; it does not return a mutable widget object. Pass the initial HTML as the fourth argument and update a plugin-owned element with Agry.dom.setHTML(elementId, html) (or use the html method on a DOM handle).

Coordinate marker non valide

Agry.addMarker uses positional arguments. Calling it with an options object makes the latitude and longitude become NaN, which the host correctly rejects. Use Agry.addMarker(latitude, longitude, popupContent, iconSource, alertLevel) and pass finite coordinates in the valid latitude/longitude ranges.

ID finestra non consentito

Agry.createWindow also uses positional arguments. The first argument must be a stable window ID matching [A-Za-z][A-Za-z0-9_-]{0,63} and must not be a reserved host ID. Passing an object produces the string [object Object], which is rejected by the secure bridge. Use Agry.createWindow("my-plugin-panel", title, contentHTML, width, height).

Capability denied

The user has not granted the requested scope or it was revoked. Show an explanatory empty state and continue with the capabilities that remain available.

Plugin source unavailable

Verify the stable plugin ID, cloud catalog entry, published source, and review status. Local legacy metadata does not automatically provide Store source.

Unexpected UI behaviour

Check for direct browser globals, unscoped selectors, duplicate event registration, or timers that were not cleared. Use stable IDs and the scoped DOM bridge.

Network request blocked

Confirm HTTPS, host allowlisting, request size, method, headers, body, and request budget. Do not attempt to bypass the firewall with XHR, WebSocket, a data URL, or a second proxy.

14. Compatibility checklist

Before submitting a plugin:

  • Read Agry.version and use feature detection for optional methods.
  • Test with no fields, no activities, no warehouse items, and no devices.
  • Test denied capabilities and expired sessions.
  • Test long text, missing names, old records, and invalid dates.
  • Test a slow or failed external service.
  • Stop and restart the plugin to verify cleanup.
  • Confirm that no credentials or tokens are included in source or logs.
  • Run the Developer Console security scan and resolve every warning.
  • Verify metadata, screenshots, privacy disclosure, and requested capabilities.
  • Keep third-party behaviour within the isolated facade documented here.

15. Versioning note

This reference describes the implementation available in the repository at the time it was generated. The host may add APIs or tighten limits without changing old plugin data. Treat undocumented methods as private, avoid depending on internal PHP endpoints, and provide a graceful fallback whenever a method or capability is unavailable.