diff --git a/.changeset/docs-skills-sync-9-4.md b/.changeset/docs-skills-sync-9-4.md new file mode 100644 index 0000000000..96a73008e2 --- /dev/null +++ b/.changeset/docs-skills-sync-9-4.md @@ -0,0 +1,6 @@ +--- +--- + +docs(skills): sync objectstack-* skills with 9.0→9.4 changes. Documentation-only +update to the `skills/` authoring guides — no package version impact (empty +changeset to satisfy the changeset check). diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index b530f27851..1acb2869a0 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -57,6 +57,35 @@ Agent → Skill → Tool > agents is supported but considered legacy. Skills provide better > discoverability, instruction scoping, and reuse. +### Built-in Unified Assistant (ADR-0040) + +The runtime ships a unified persona agent **`data_chat`** (`DEFAULT_DATA_AGENT_NAME`) +— the implicit default copilot for any app that doesn't pin `app.defaultAgent` +(Studio overrides to `metadata_assistant`). It attaches built-in skills: + +- **`data_explorer`** — read-only data Q&A: `list_objects`, `describe_object`, + `query_data` / `query_records`, `get_record`, `aggregate_data`, `visualize_data`. +- **`metadata_authoring`** + **`solution_design`** — app-building, **cloud/EE only**. + +To grant data exploration to your own agent, add `data_explorer` to its `skills[]`; +deactivating that skill (`active: false`) revokes data Q&A for every agent that +references it. + +> **Edition gate (#1803):** when neither `metadata_authoring` nor `solution_design` +> is registered — the open single-env framework — the assistant is **query-only** +> and declines build/authoring requests. App-building tools are supplied entirely +> by the cloud AI Studio plugin; do not assume they resolve in the open framework. + +> **`visualize_data` (#1820/#1821):** the only built-in tool that draws a chart — +> it aggregates an object and emits an inline `data-chart` part. Auto-registered +> **only** when an analytics service (`IAnalyticsService`) is wired; `query_data` / +> `aggregate_data` return numbers, not charts. + +> **Ops:** set `AI_DAILY_USER_MESSAGES=` to cap user turns per user per day +> (ADR-0040 §5; backed by the `ai_usage_daily` object, no-op if unset). Adapter +> health is observable at `GET /api/v1/ai/status`; invalid `ai` settings are +> rejected at save time (#1788). + --- ## Agent Configuration @@ -110,9 +139,9 @@ export default defineAgent({ temperature: 0.3, }, guardrails: { - blockedTopics: ['internal_pricing', 'employee_data'], - maxTurns: 20, - requireApprovalFor: ['delete_record', 'escalate'], + blockedTopics: ['internal_pricing', 'employee_data'], // forbidden topics / action names + maxTokensPerInvocation: 8000, // token budget per invocation + maxExecutionTimeSec: 60, // wall-clock cap per invocation }, }); ``` @@ -322,6 +351,11 @@ Retrieval-Augmented Generation gives agents access to domain knowledge. | `azure_openai` | Same as OpenAI, enterprise managed | Compliance, data residency | | `local` | Ollama, vLLM, llama.cpp | On-premise, air-gapped | +> The inline agent `model.provider` enum is the narrow set above +> (`openai` / `azure_openai` / `anthropic` / `local`). **Model-registry** entries +> (`ModelProviderSchema`) accept a wider set: also `google`, `cohere`, +> `huggingface`, `custom`. + ### Model Selection Guidelines | Scenario | Recommended | diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index a6fce7bbd4..d885b04a2f 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -59,6 +59,29 @@ GET /api/v1/{object}/aggregate # Aggregation queries > **Key rule:** If your object defines `apiMethods`, only those operations are > exposed. For example, `apiMethods: ['get', 'list']` creates a read-only API. +### Metadata API (`/meta`) + +The metadata read surface lives under `/api/v1/meta` (separate from the data +CRUD routes above): + +``` +GET /api/v1/meta/:type # List metadata items of a type (object, view, flow, doc, …) +GET /api/v1/meta/:type/:name # Read a single metadata item +``` + +Three query-param contracts gained in 9.x: + +- **`?preview=draft`** (#1763) — overlay pending **draft** metadata instead of the + published copy, on both list and get. The draft path is **cache-bypassed**, so + it always reflects the latest unpublished edit (ADR-0033/0037 authoring loop). +- **`?package=`** (ADR-0048, #1816/#1819) — **package-scope** a read so + two installed packages that share a bare metadata name disambiguate by owning + package; prefer-local resolution. A package-scoped read **bypasses the meta + cache**. The layered / Studio-editor read is package-scoped the same way. +- **`/meta/doc`** (ADR-0046, #1790) — docs-as-metadata. The **list** response omits + each doc's `content` by default (use `?include=content` to include it); the + **single-item** `GET /meta/doc/:name` always returns the full body. + ### Public (anonymous) Form Endpoints Any `FormView` declared with `sharing.allowAnonymous: true` and a diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 84d65d8969..3ed1e4ebff 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -57,7 +57,7 @@ parallel. Flows are the primary automation building block in ObjectStack. | `screen` | Interactive — presents UI screens to the user (wizards, forms) | | `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) | | `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`) | -| `api` | Invoked explicitly via the API / `engine.execute()` | +| `api` | Invoked explicitly via the API / `engine.execute()`, **or** bound as an inbound **webhook**: `POST /api/v1/automation/hooks/:flowName/:hookId` (see *Inbound webhook triggers* below) | ### Flow Node Types @@ -342,6 +342,7 @@ branch — you never resume the flow by hand. | `lockRecord` | Lock the triggering record from edits while pending. Default `true` | | `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) | | `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }` | +| `maxRevisions` | ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge | ### Branching, side-effects & rejection @@ -353,6 +354,12 @@ These are wired on the **graph**, not in node config: `http_request`, an email node, …) to the `approve` / `reject` out-edge. - **Roll back on reject** — route the `reject` edge as a **back-edge** to an earlier node so the submitter can revise (the old `back_to_previous`). +- **Send back for revision (ADR-0044)** — distinct from a plain reject: an + Approval node can emit a third decision **`revise`** on a `revise`-labeled + out-edge that routes to a rework wait point. The submitter edits and + resubmits, re-entering the node via an edge `type: 'back'` (a declared + back-edge — traversed at run time but excluded from DAG cycle validation). + `maxRevisions` (node config, default `3`) caps the loop before auto-reject. - **Hard reject** — route the `reject` edge to an `end` node (the old `reject_process`). @@ -381,17 +388,38 @@ ObjectQL lifecycle hook. ### Prerequisite — enable the `triggers` capability -Record-change (and schedule) triggers ship as a plugin gated behind the +Record-change, schedule, **and inbound-webhook (`api`)** triggers ship behind the `triggers` capability. **Without it the flows register but never fire.** Add it -to the package config (pair with `job` for schedule/cron triggers): +to the package config: ```typescript defineStack({ // … - requires: ['automation', 'triggers'], // + 'job' for scheduled flows + requires: ['automation', 'triggers'], + // + 'job' for scheduled (cron) flows + // + 'queue' for inbound-webhook ('api') flows — the trigger-api plugin + // depends on the queue service; without it every inbound POST + // returns 503 queue_unavailable. }); ``` +### Inbound webhook (`api`) triggers (ADR-0041 Tier 1) + +An `api` flow can be bound to an inbound HTTP endpoint: +`POST /api/v1/automation/hooks/:flowName/:hookId`. Configure it on the **start +node `config`** (the start `config` is a free-form record, so these keys are +read at runtime, not Zod-validated): + +| `config` key | Purpose | +|:-------------|:--------| +| `hookId` | URL path token (default `'default'`). **Rotate it to revoke** a leaked endpoint | +| `secret` | HMAC-SHA256 shared secret. Strongly recommended — without it unsigned posts are accepted and a warning is logged | + +- **Signature:** sender sends `x-objectstack-signature: sha256=` (GitHub/Stripe style). +- **Idempotency:** `x-idempotency-key` dedupes retries — author the flow to be idempotent (delivery is at-least-once). +- **Queue-backed:** the endpoint ACKs `202` and enqueues; the flow runs on the consumer, never in-band. Requires the `queue` service (see prerequisite). +- The JSON body surfaces to the flow as the trigger record (`record.*` / bare fields) plus `params`. + ### Trigger Types (start-node `config.triggerType`) | `triggerType` | Fires | ObjectQL hook | diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 61b09db70c..ef72ec94b2 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -695,6 +695,12 @@ For `lookup` fields, supply the **natural key** of the target record (not its UUID). The seed runner resolves at load time. Order datasets so parents appear before children in the exported array: +> If a lookup value matches no natural key, the loader now falls back to +> resolving it as the target's `id` (#1814) — so a reference to a real existing +> record by internal id resolves instead of dangling to null. Natural keys +> remain the portable default; rely on the id fallback only for records you +> didn't seed (e.g. a system user). + ```typescript const contacts = defineDataset(Contact, { externalId: 'email', diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 0cb12865e5..69b32418c0 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -297,7 +297,9 @@ manifest: { } ``` -**Object naming:** The object `name` is the canonical identifier and equals the physical table name. If you want a domain prefix, embed it directly in the name (e.g. `name: 'crm_account'`). There is no automatic namespace prefixing. +**Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is deprecated and ignored by the runtime. + +**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `__` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set. --- @@ -419,12 +421,14 @@ CLI: `os serve` / `os dev` ├── AppPlugin (loads the defineStack bundle) ├── I18nServicePlugin (if translations/i18n defined) ├── AuthPlugin + ├── Split platform-app plugins (ADR-0048, optional/best-effort, after AuthPlugin): + │ @objectstack/setup → createSetupAppPlugin (first-run wizard) + │ @objectstack/studio → createStudioAppPlugin + │ @objectstack/account → createAccountAppPlugin ├── HonoServerPlugin - ├── SetupPlugin (first-run wizard) ├── RESTPlugin (auto-generated API) ├── DispatcherPlugin - ├── AIServicePlugin (if available) - └── StudioPlugin (if --ui flag) + └── AIServicePlugin (if available) 5. Runtime.start() → init + start all plugins 6. Server listens on the resolved port (see "Ports & networking" in Part 3) ``` @@ -958,6 +962,10 @@ describe('AuditPlugin', () => { | `com.objectstack.metadata` | `metadata` | `@objectstack/metadata` | | `com.objectstack.realtime` | `realtime` | `@objectstack/service-realtime` | | `com.objectstack.cache` | `cache` | `@objectstack/service-cache` | +| `com.objectstack.setup` | — | `@objectstack/setup` → `createSetupAppPlugin` (ADR-0048 one-app pkg) | +| `com.objectstack.studio` | — | `@objectstack/studio` → `createStudioAppPlugin` | +| `com.objectstack.account` | — | `@objectstack/account` → `createAccountAppPlugin` | +| `com.objectstack.cloud-connection` | — | `@objectstack/cloud-connection` → `createCloudConnectionPlugin` | --- @@ -968,6 +976,8 @@ metadata is read-only and artifact/file backed: - Do **not** register `sys_metadata` or `sys_metadata_history` from an ObjectOS runtime plugin. Those persistence tables belong to the control plane. + (Exception, #1826: an *isolated project kernel* may opt into `sys_metadata` + hydration from its own DB — the general boundary otherwise stands.) - Do **not** call `MetadataManager.setDataEngine()` automatically from `MetadataPlugin.start()`. Project databases must contain business rows only. - Use `artifactSource: { mode: 'local-file', path: './dist/objectstack.json' }` @@ -1124,6 +1134,17 @@ Port resolution is the same for `os dev` and `os start` (both spawn `os serve`): | `os publish` | Push the compiled stack to a cloud environment | | `os register` | Register the local stack as a deployable target | | `os cloud …` | Cloud-specific subcommands (logs, metrics, status) | +| `os package publish [dist/objectstack.json] [--env … --install --visibility org]` | Upload the compiled artifact as a versioned package to the cloud catalog (ADR-0008 P3) | +| `os package install [--version │ --runtime http://localhost:3000]` | Install a package into a **running** runtime via its install-local endpoint. Catalog mode (by manifest id) or air-gapped local-artifact mode. Auths with the **target runtime's** session (`--email/--password` or `OS_RUNTIME_EMAIL`/`OS_RUNTIME_PASSWORD`), not the cloud login | + +> **Cloud connection & marketplace (`@objectstack/cloud-connection`, ADR-0008/0009).** +> The open runtime-side cloud client. Its plugins — +> `CloudConnectionPlugin`/`createCloudConnectionPlugin`, `MarketplaceProxyPlugin`, +> `MarketplaceInstallLocalPlugin`, `RuntimeConfigPlugin` — expose the install-local +> endpoint that `os package install` targets, ship the **Installed Apps** page and +> marketplace Setup nav as plugin metadata, and maintain `LocalManifestSource` +> (a local desired-state ledger) plus runtime-identity bind v2 (environment-less +> self-hosted binding). ## Testing pattern diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 1b50b4db22..5dd2d3e945 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -278,6 +278,8 @@ Rules: so most views need no filter elements at all. - `userFilters: { element: 'dropdown' }` (no `fields`) is valid shorthand: the renderer fills the field list from the object's select/boolean fields. +- `element` is `dropdown` or `tabs`; `toggle` is **deprecated** (ADR-0047 §3.4a) + — it stays in the enum for back-compat rendering, but author `dropdown`/`tabs`. - The visualization switcher renders as a compact dropdown in the toolbar's right cluster. Authors only control the `allowedVisualizations` whitelist; a single-entry whitelist locks the visualization (no switcher). @@ -328,6 +330,9 @@ export const CrmApp = App.create({ label: 'Enterprise CRM', icon: 'briefcase', defaultAgent: 'sales_copilot', // optional AI copilot binding + // hidden: true, // ADR-0045 — drop from the App Switcher but keep + // routable & permission-checked; the shell surfaces + // hidden apps (e.g. `account`) via the avatar menu. branding: { primaryColor: '#4169E1', logo: '/assets/crm-logo.png', @@ -440,7 +445,7 @@ filter; keep `filter` on the widget when binding a dataset. |:-----|:------------| | `tabular` | Flat data table with columns and filters | | `summary` | Grouped data with subtotals (e.g., revenue by region) | -| `matrix` | Cross-tab / pivot table (two grouping dimensions) | +| `matrix` | Cross-tab / pivot table (`rows` down × `columns` across) | | `chart` | Visual chart report | | `joined` | Multi-block analytic surface (combines several sub-reports) | @@ -458,9 +463,11 @@ export const PipelineCoverageReport: ReportInput = { label: 'Pipeline Coverage (Quarter)', type: 'matrix', dataset: 'opportunity_metrics', - rows: ['forecast_category', 'close_date'], - values: ['amount_sum'], + rows: ['forecast_category'], // down axis + columns: ['close_date'], // across axis (ADR-0021 D2) — matrix pivots rows × columns + values: ['amount_sum'], // measures placed in the cells runtimeFilter: { stage: { $ne: 'closed_lost' } }, + // drilldown defaults true — click a cell to open the underlying records; set false to disable. chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount_sum' }, }; ``` @@ -470,8 +477,11 @@ export const PipelineCoverageReport: ReportInput = { > field server-side in a single aggregate query — do **not** pre-compute virtual > columns for this. > **`rows`** are the report's grouping dimensions (selected from the dataset by -> name). A `summary` groups by them; a `matrix` cross-tabs them. Multi-level -> grouping = multiple dimension names in the array. +> name). A `summary` groups *down* by `rows`. A `matrix` pivots `rows` (down) × +> **`columns`** (across, ADR-0021 D2) with `values` in the cells — do **not** +> put both axes in `rows`. Multi-level grouping on either axis = multiple +> dimension names in that array. `drilldown` (default `true`) makes cells +> click-through to the underlying records. --- @@ -1096,6 +1106,29 @@ export const AddToCampaignAction: Action = { }; ``` +### Opening in a New Tab (`opensInNewTab` / `newTabUrl`) + +For actions that should land in a new browser tab, set `opensInNewTab: true` +(#1787). The renderer pre-opens the tab **synchronously** on click so popup +blockers don't fire, then navigates it to the handler's returned `redirectUrl`. + +For external deep-links / SSO with no server round-trip, add `newTabUrl` — a +direct URL template (supports the `{recordId}` placeholder). It is valid **only** +alongside `opensInNewTab: true`, and the target endpoint must enforce its own +auth (the new tab carries no in-app session context). + +```typescript +export const OpenInvoicePdfAction: Action = { + name: 'open_invoice_pdf', + label: 'Open PDF', + objectName: 'invoice', + type: 'url', + opensInNewTab: true, + newTabUrl: '/api/v1/invoice/{recordId}/pdf', // zero-roundtrip; endpoint self-auths + locations: ['record_header'], +}; +``` + ### Action Parameter Patterns Prefer **field-backed** params (`{ field: 'email' }`) over inline declarations