@sparkerp/plugin-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/bundle/blocks.json +12229 -0
  4. package/bundle/catalog.json +32475 -0
  5. package/bundle/docs/applications/hcm/employee-profile.md +144 -0
  6. package/bundle/docs/concepts/artifact-types.md +80 -0
  7. package/bundle/docs/concepts/glossary.md +124 -0
  8. package/bundle/docs/concepts/overview.md +88 -0
  9. package/bundle/docs/concepts/page-data-flow.md +84 -0
  10. package/bundle/docs/concepts/plugin-lifecycle.md +81 -0
  11. package/bundle/docs/concepts/sdk-modes.md +71 -0
  12. package/bundle/docs/docs.json +109 -0
  13. package/bundle/docs/guides/add-a-cadence-job.md +134 -0
  14. package/bundle/docs/guides/add-a-compliance-job.md +139 -0
  15. package/bundle/docs/guides/add-a-create-edit-form.md +167 -0
  16. package/bundle/docs/guides/add-a-custom-block.md +225 -0
  17. package/bundle/docs/guides/add-a-data-provider.md +216 -0
  18. package/bundle/docs/guides/add-a-kpi.md +154 -0
  19. package/bundle/docs/guides/add-a-reminder-job.md +181 -0
  20. package/bundle/docs/guides/add-an-aggregation-job.md +153 -0
  21. package/bundle/docs/guides/add-an-approval-workflow.md +222 -0
  22. package/bundle/docs/guides/add-an-entity.md +170 -0
  23. package/bundle/docs/guides/add-business-rules.md +147 -0
  24. package/bundle/docs/guides/add-i18n.md +121 -0
  25. package/bundle/docs/guides/add-menus.md +149 -0
  26. package/bundle/docs/guides/build-a-code-plugin.md +141 -0
  27. package/bundle/docs/guides/build-a-page.md +213 -0
  28. package/bundle/docs/guides/build-a-polyglot-service-plugin.md +201 -0
  29. package/bundle/docs/guides/build-a-tenant-extension-service.md +149 -0
  30. package/bundle/docs/guides/create-a-plugin.md +172 -0
  31. package/bundle/docs/guides/expose-a-plugin-operation-as-an-ai-tool.md +114 -0
  32. package/bundle/docs/guides/index.md +53 -0
  33. package/bundle/docs/guides/publish-and-upgrade.md +152 -0
  34. package/bundle/docs/guides/responsive-plugin.md +117 -0
  35. package/bundle/docs/guides/seed-data-on-install.md +127 -0
  36. package/bundle/docs/guides/set-up-the-sdk.md +120 -0
  37. package/bundle/docs/guides/use-the-mcp-server.md +104 -0
  38. package/bundle/docs/guides/validate-and-test.md +119 -0
  39. package/bundle/docs/guides/wire-a-pages-data.md +153 -0
  40. package/bundle/docs/quickstart/index.md +118 -0
  41. package/bundle/docs/recipes/condition-based-compliance-flag.md +70 -0
  42. package/bundle/docs/recipes/cross-plugin-offboarding-fanout.md +68 -0
  43. package/bundle/docs/recipes/cross-row-aggregation.md +67 -0
  44. package/bundle/docs/recipes/entity-with-approval-workflow.md +72 -0
  45. package/bundle/docs/recipes/index.md +40 -0
  46. package/bundle/docs/recipes/install-lifecycle-data-seeding.md +76 -0
  47. package/bundle/docs/recipes/kpi-dashboard-page.md +92 -0
  48. package/bundle/docs/recipes/master-detail-page.md +84 -0
  49. package/bundle/docs/recipes/n-days-before-reminder.md +72 -0
  50. package/bundle/docs/recipes/per-row-document-generator.md +65 -0
  51. package/bundle/docs/recipes/related-record-lookup-field.md +74 -0
  52. package/bundle/docs/recipes/resolved-reference-grid-column.md +63 -0
  53. package/bundle/docs/recipes/scheduled-record-generation.md +68 -0
  54. package/bundle/docs/reference/action.md +18 -0
  55. package/bundle/docs/reference/block-definition.md +35 -0
  56. package/bundle/docs/reference/block-instance.md +33 -0
  57. package/bundle/docs/reference/composite-pack-manifest.md +33 -0
  58. package/bundle/docs/reference/connector-definition.md +36 -0
  59. package/bundle/docs/reference/data-service-definition.md +22 -0
  60. package/bundle/docs/reference/data-view-definition.md +26 -0
  61. package/bundle/docs/reference/entity-aggregation-config.md +38 -0
  62. package/bundle/docs/reference/entity-cadence-config.md +33 -0
  63. package/bundle/docs/reference/entity-compliance-config.md +37 -0
  64. package/bundle/docs/reference/entity-cross-plugin-action-config.md +37 -0
  65. package/bundle/docs/reference/entity-definition.md +18 -0
  66. package/bundle/docs/reference/entity-document-generator-config.md +35 -0
  67. package/bundle/docs/reference/entity-rule-definition.md +29 -0
  68. package/bundle/docs/reference/entity-status-date-sweep-config.md +27 -0
  69. package/bundle/docs/reference/form.md +35 -0
  70. package/bundle/docs/reference/index.md +37 -0
  71. package/bundle/docs/reference/menu.md +26 -0
  72. package/bundle/docs/reference/mobile-nav.md +22 -0
  73. package/bundle/docs/reference/page-template.md +29 -0
  74. package/bundle/docs/reference/page.md +44 -0
  75. package/bundle/docs/reference/plugin-manifest.md +54 -0
  76. package/bundle/docs/reference/plugin-seed-data.md +21 -0
  77. package/bundle/docs/reference/print-template-definition.md +22 -0
  78. package/bundle/docs/reference/provider-definition.md +22 -0
  79. package/bundle/docs/reference/report-definition.md +22 -0
  80. package/bundle/docs/reference/rule.md +30 -0
  81. package/bundle/docs/reference/workflow-definition.md +28 -0
  82. package/bundle/docs/troubleshooting/grid-shows-zero-rows.md +82 -0
  83. package/bundle/docs/troubleshooting/job-never-runs.md +89 -0
  84. package/bundle/docs/troubleshooting/known-gotchas.md +193 -0
  85. package/bundle/docs/troubleshooting/publishing-errors.md +119 -0
  86. package/bundle/docs/tutorial/01-create-the-plugin.md +157 -0
  87. package/bundle/docs/tutorial/02-category-entity-and-seed.md +94 -0
  88. package/bundle/docs/tutorial/03-equipment-entity.md +121 -0
  89. package/bundle/docs/tutorial/04-equipment-register-page.md +153 -0
  90. package/bundle/docs/tutorial/05-checkout-entity-and-page.md +116 -0
  91. package/bundle/docs/tutorial/06-checkout-approval-workflow.md +139 -0
  92. package/bundle/docs/tutorial/07-return-due-reminder-job.md +129 -0
  93. package/bundle/docs/tutorial/08-menus-i18n-publish.md +123 -0
  94. package/bundle/docs/tutorial/index.md +72 -0
  95. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-aggregation-configs.json +18 -0
  96. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cadence-configs.json +9 -0
  97. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-compliance-configs.json +74 -0
  98. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cross-plugin-configs.json +9 -0
  99. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-document-configs.json +9 -0
  100. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-sweep-configs.json +260 -0
  101. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/entities/hcm_salary_planning_entry.json +41 -0
  102. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/page/salary-planning.json +1 -0
  103. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/page/promotions.json +10944 -0
  104. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.movement.json +44 -0
  105. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.promotion-acknowledge.json +19 -0
  106. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.resignation.json +29 -0
  107. package/bundle/examples/example-patterns.mjs +94 -0
  108. package/bundle/manifest.json +14 -0
  109. package/bundle/schemas/action.schema.json +102 -0
  110. package/bundle/schemas/block-definition.schema.json +222 -0
  111. package/bundle/schemas/block-instance.schema.json +139 -0
  112. package/bundle/schemas/composite-pack-manifest.schema.json +160 -0
  113. package/bundle/schemas/connector-definition.schema.json +29 -0
  114. package/bundle/schemas/data-service-definition.schema.json +64 -0
  115. package/bundle/schemas/data-view-definition.schema.json +148 -0
  116. package/bundle/schemas/entity-aggregation-config.schema.json +32 -0
  117. package/bundle/schemas/entity-cadence-config.schema.json +27 -0
  118. package/bundle/schemas/entity-compliance-config.schema.json +31 -0
  119. package/bundle/schemas/entity-cross-plugin-action-config.schema.json +31 -0
  120. package/bundle/schemas/entity-definition.schema.json +126 -0
  121. package/bundle/schemas/entity-document-generator-config.schema.json +29 -0
  122. package/bundle/schemas/entity-rule-definition.schema.json +75 -0
  123. package/bundle/schemas/entity-status-date-sweep-config.schema.json +21 -0
  124. package/bundle/schemas/form.schema.json +281 -0
  125. package/bundle/schemas/menu.schema.json +106 -0
  126. package/bundle/schemas/mobile-nav.schema.json +53 -0
  127. package/bundle/schemas/page-template.schema.json +46 -0
  128. package/bundle/schemas/page.schema.json +292 -0
  129. package/bundle/schemas/plugin-manifest.schema.json +139 -0
  130. package/bundle/schemas/plugin-seed-data.schema.json +24 -0
  131. package/bundle/schemas/print-template-definition.schema.json +90 -0
  132. package/bundle/schemas/provider-definition.schema.json +27 -0
  133. package/bundle/schemas/report-definition.schema.json +96 -0
  134. package/bundle/schemas/rule.schema.json +82 -0
  135. package/bundle/schemas/workflow-definition.schema.json +42 -0
  136. package/bundle/validators/block-engine.mjs +23222 -0
  137. package/bundle/validators/page-engine.mjs +21645 -0
  138. package/erp-cli/authoring-root.mjs +344 -0
  139. package/erp-cli/erp.mjs +4065 -0
  140. package/erp-cli/example-patterns.mjs +94 -0
  141. package/erp-mcp-server/index.mjs +818 -0
  142. package/package.json +27 -0
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: Build a tenant extension service (L5)
3
+ audience: tenant
4
+ ---
5
+
6
+ # Build a tenant extension service (L5)
7
+
8
+ ## What you're doing
9
+
10
+ Every guide before this one builds something that runs **inside** the shared
11
+ ERP — a plugin's Java code loads into the same JVM as every other tenant's.
12
+ An L5 extension is different: it is **your own separate process**, running
13
+ in your own container, that the ERP core calls over plain HTTP whenever your
14
+ tenant has one installed and enabled for a given extension point. The core
15
+ never loads your code, never shares your JVM, and keeps working exactly as
16
+ before if your service is down, slow, or was never installed at all.
17
+
18
+ Use this when a plugin isn't enough — you need to run code the platform
19
+ can't vet and load in-process (a proprietary calculation, a call to your own
20
+ internal systems, dependencies that would never be approved into the shared
21
+ runtime).
22
+
23
+ | Artifact | What it is | Where it runs |
24
+ | --- | --- | --- |
25
+ | A regular plugin (every other guide) | Java/TS code, installed into the shared ERP | In-process, shared JVM |
26
+ | An **L5 extension service** (this guide) | Your own Spring Boot app | Your own process/container |
27
+
28
+ ## The complete example
29
+
30
+ Copy the real, runnable skeleton at
31
+ [`extensions/archetype/`](../../extensions/archetype) — don't hand-write one.
32
+ The fastest way:
33
+
34
+ ```bash
35
+ erp extension create-service acme-payroll
36
+ cd extensions/acme-payroll-extension-service
37
+ mvn spring-boot:run
38
+ ```
39
+
40
+ This scaffolds a small Spring Boot app with exactly one endpoint:
41
+
42
+ ```java
43
+ @RestController
44
+ @RequestMapping("/extension-api")
45
+ public class ExtensionApiController {
46
+
47
+ @PostMapping("/{extensionPointCode}")
48
+ public ResponseEntity<Map<String, Object>> invoke(
49
+ @PathVariable String extensionPointCode,
50
+ @RequestHeader(value = "Authorization", required = false) String authorization,
51
+ @RequestBody(required = false) Map<String, Object> request) {
52
+ if (!serviceAuthTokenVerifier.verify(authorization)) {
53
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "invalid token"));
54
+ }
55
+ // request is a FLAT map: your payload fields + tenantId/applicationId/
56
+ // extensionId/actor/correlationId merged in — see the file's own doc comment.
57
+ Map<String, Object> response = new LinkedHashMap<>();
58
+ // ... your logic here, keyed on extensionPointCode ...
59
+ return ResponseEntity.ok(response); // fields to merge into the caller's record
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## The wire contract, exactly
65
+
66
+ The ERP core's `ExtensionRouter` calls your service like this — match it
67
+ exactly, there is no other shape:
68
+
69
+ - **Request**: `POST {your-endpoint}/extension-api/{extensionPointCode}` — a
70
+ FLAT JSON body (your business fields, plus `tenantId`/`applicationId`/
71
+ `extensionId`/`actor`/`correlationId` always present and always trustworthy
72
+ — never read a tenant id from anywhere else). Headers carry
73
+ `Authorization: Bearer <token>` (see Security below), `X-Tenant-Id`,
74
+ `X-Correlation-Id`.
75
+ - **Response**: any 2xx with a FLAT JSON body = "extended" — return only the
76
+ field(s) you want merged into the record. `{}` is a valid, successful
77
+ "no changes" response.
78
+ - **Timeout**: 2 seconds. The core wraps every call in a circuit breaker —
79
+ a slow or broken service degrades to "not extended," it never breaks the
80
+ tenant's request.
81
+ - Today the ERP fires exactly ONE extension point:
82
+ `EntityRecordBeforeCreate` — before a new Entity Engine record is
83
+ inserted, for ANY tenant-designed entity in ANY application.
84
+
85
+ ## Security
86
+
87
+ Every call from the core carries a short-lived (30s) HMAC-SHA256 signed
88
+ bearer token. Set the SAME secret on both sides:
89
+
90
+ ```bash
91
+ # ERP core (env on the erp-suite/engine-api process)
92
+ ERP_EXTENSION_SERVICE_AUTH_SECRET=<your real shared secret>
93
+
94
+ # your extension service
95
+ ERP_EXTENSION_SERVICE_AUTH_SECRET=<the SAME shared secret>
96
+ ```
97
+
98
+ Never ship the `dev-only-...` default to production. This is a shared
99
+ secret, not certificate-based mTLS — see the archetype's own README
100
+ "Security model" section for exactly what's real vs. deliberately deferred.
101
+
102
+ ## Ground yourself first
103
+
104
+ ```bash
105
+ erp extension create-service --help
106
+ erp platform describe RemoteExtensionPoint
107
+ ```
108
+
109
+ ## Register and enable it
110
+
111
+ Your service does nothing until it's installed AND enabled for a tenant —
112
+ until then, the core never calls it at all.
113
+
114
+ ```bash
115
+ erp tenant plugin install <tenantId> acme-payroll --runtime service \
116
+ --endpoint https://<your-service-host>:8480 --application HCM
117
+ erp tenant plugin enable <tenantId> acme-payroll --application HCM
118
+ ```
119
+
120
+ ## How to verify it worked
121
+
122
+ ```bash
123
+ curl -s -X POST localhost:8480/extension-api/EntityRecordBeforeCreate \
124
+ -H 'content-type: application/json' \
125
+ -H 'authorization: Bearer <token minted with your shared secret>' \
126
+ -d '{"tenantId":42,"applicationId":"HCM","extensionId":"EntityRecordBeforeCreate","actor":"system","correlationId":"demo","employeeId":100234}'
127
+ ```
128
+
129
+ ```json
130
+ {}
131
+ ```
132
+
133
+ Then create a record in that tenant's app that goes through the Entity
134
+ Engine's create path — your service's logs should show the call arriving.
135
+
136
+ ## Common mistakes
137
+
138
+ | Symptom | Cause | Fix |
139
+ | --- | --- | --- |
140
+ | Service never gets called | Binding not `ENABLED`, or you're on the wrong `applicationId` | `erp tenant plugin list <tenantId>` to check status |
141
+ | `401` on every call | Shared secret mismatch | Set the exact same `ERP_EXTENSION_SERVICE_AUTH_SECRET` on both sides |
142
+ | Core logs "security violation" | Your response's `tenantId` field doesn't match the request | Don't echo `tenantId` back at all unless it's unchanged |
143
+ | Change never takes effect for up to ~45s | Binding resolution is cached | Expected — bounded staleness, not a bug |
144
+ | Deployed extension can't reach anything else in the cluster | `NetworkPolicy` default-denies egress | Correct by design — add one narrow rule if you truly need an external call |
145
+
146
+ ## What to read next
147
+
148
+ - [`extensions/archetype/README.md`](../../extensions/archetype/README.md) — full security model, Docker/Kubernetes deployment
149
+ - [Publish and upgrade a plugin](./publish-and-upgrade.md) — for the regular (in-process) plugin path this guide is the alternative to
@@ -0,0 +1,172 @@
1
+ ---
2
+ title: Create a plugin from scratch
3
+ audience: tenant
4
+ ---
5
+
6
+ # Create a plugin from scratch
7
+
8
+ ## What you're doing
9
+
10
+ A **plugin** is a folder of JSON metadata that the ERP's generic engines
11
+ execute — no Java required. You declare what you want — a
12
+ table here, a screen there, an approval there — and the platform does the rest.
13
+ This guide gets you an empty, valid, buildable plugin.
14
+
15
+ ## The complete sequence
16
+
17
+ ```bash
18
+ erp plugin create office-equipment --name "Office Equipment" --type business-app
19
+ erp menu create office-equipment office-equipment-menu \
20
+ --display-name "Equipment" --route /office-equipment/equipment-register
21
+ ```
22
+
23
+ That produces:
24
+
25
+ ```
26
+ office-equipment/
27
+ ├── README.md
28
+ └── spk-assembly/
29
+ ├── plugin.json <- the manifest
30
+ └── metadata/
31
+ ├── entities/ <- one *.json per table
32
+ ├── page/ <- one *.json per screen
33
+ ├── provider/ <- REST data providers for grids
34
+ ├── data_service/ <- parameterized read queries
35
+ ├── data_view/ <- SQL joins over physical tables
36
+ ├── rules/ <- lifecycle rules
37
+ ├── workflow/ <- approval processes
38
+ ├── seed-data/ <- rows to upsert on install
39
+ ├── menu/ <- navigation
40
+ └── i18n/ <- en.json etc.
41
+ ```
42
+
43
+ `spk-assembly/` is the build root. The `.spk` file `erp plugin build` produces
44
+ is just a zip of this tree.
45
+
46
+ ## The manifest (`spk-assembly/plugin.json`)
47
+
48
+ Here is a complete, real manifest — the one the [tutorial module](../tutorial/example-plugin/spk-assembly/plugin.json) ships:
49
+
50
+ ```json
51
+ {
52
+ "id": "office-equipment",
53
+ "name": "Office Equipment",
54
+ "version": "1.0.3",
55
+ "type": "business-application",
56
+ "schemaName": "erp_core",
57
+ "vendor": "ACME Corp",
58
+ "license": "Proprietary",
59
+ "licenseTier": "free",
60
+ "category": "custom",
61
+ "erpVersion": "*",
62
+ "minErpVersion": "1.0",
63
+ "maxErpVersion": "99.0",
64
+ "mainClass": null,
65
+ "dependencies": [],
66
+ "optionalDependencies": [],
67
+ "owner": "business",
68
+ "editable": false,
69
+ "extendable": true,
70
+ "capabilitiesProvided": [],
71
+ "capabilitiesRequired": [],
72
+ "featureFlags": [],
73
+ "configSchemaJson": null,
74
+ "roles": [
75
+ {
76
+ "name": "Office Equipment Manager",
77
+ "code": "OFFICE_EQUIPMENT_MANAGER",
78
+ "description": "Full control of the Office Equipment module.",
79
+ "permissionKeys": [
80
+ "OeqEquipment.View", "OeqEquipment.Create", "OeqEquipment.Edit",
81
+ "OeqCheckout.View", "OeqCheckout.Create", "OeqCheckout.Submit", "OeqCheckout.Approve"
82
+ ]
83
+ }
84
+ ],
85
+ "runtimeModes": ["embedded"],
86
+ "serviceDeployment": null
87
+ }
88
+ ```
89
+
90
+ ### Line by line
91
+
92
+ - **`id`** — globally unique, lowercase kebab-case. It's baked into every route,
93
+ i18n key, and page id. **Choose it once.** Renaming later means rewriting every
94
+ file. (It is also the PF4J plugin id, and the platform cannot load two plugins
95
+ with the same id even across versions — see
96
+ [Publish and upgrade](./publish-and-upgrade.md).)
97
+ - **`version`** — semver. **Bump it before every publish** or the publish is a
98
+ silent no-op (see [known gotchas](../troubleshooting/known-gotchas.md)).
99
+ - **`type`** — `business-application` for a normal feature plugin.
100
+ - **`schemaName`** — the Postgres schema your entity tables route to. The
101
+ scaffold default is `erp_core`. See the caveat below.
102
+ - **`mainClass`** — leave it `null` (as the scaffold writes it) for a pure-JSON
103
+ plugin. Only set it if you ship your own Java extensions. See "Java: only if
104
+ you need it" below.
105
+ - **`roles`** — the roles this plugin creates on install. `code` is stable and
106
+ referenced by workflows; `name` must be unique across all installed plugins.
107
+ - **`dependencies`** — plugin ids that must already be installed. Leave `[]` for
108
+ a standalone plugin.
109
+ - **`runtimeModes: ["embedded"]`** — the plugin runs inside the ERP process. The
110
+ alternative, `"service"`, is for plugins deployed as their own container and is
111
+ out of scope here.
112
+
113
+ ## Java: only if you need it
114
+
115
+ A pure-JSON plugin ships **no Java at all**. Leave `"mainClass": null` and omit
116
+ `spk-assembly/lib/`. On install the platform loads it through a built-in generic
117
+ no-op PF4J entry point; start/stop/enable/disable/upgrade, migrations and data
118
+ seeding all run exactly as for a plugin with its own `mainClass`. This is the
119
+ default and the recommended shape.
120
+
121
+ You only add Java when a feature genuinely needs behaviour the Entity / Query /
122
+ Rule / Workflow / Job engines can't express. Then write a `Plugin` subclass,
123
+ compile it against `pf4j` + `slf4j-api`, drop the jar in `spk-assembly/lib/`,
124
+ and set `mainClass` to its fully-qualified name — `erp plugin build` zips
125
+ whatever is in `lib/` into the `.spk`.
126
+
127
+ ## The `schemaName` caveat
128
+
129
+ Entity tables get created in the schema named by `plugin.json`'s `schemaName`.
130
+ The scaffold default `erp_core` works for CRUD and for the pages in this guide,
131
+ **but** some generic write paths (notably the status-date-sweep job) assume a
132
+ tenant-scoped table and can fail against a table that isn't laid out the way an
133
+ app-schema table is. If your plugin owns entities and uses scheduled jobs,
134
+ prefer a dedicated schema name and confirm your entities behave against it with
135
+ `erp plugin test` and a live sweep before you rely on it. The tutorial uses
136
+ `erp_core` and documents where this bites.
137
+
138
+ ## How to verify it worked
139
+
140
+ ```bash
141
+ erp plugin validate office-equipment/spk-assembly
142
+ erp plugin test office-equipment/spk-assembly
143
+ ```
144
+
145
+ A fresh scaffold with no pages yet prints:
146
+
147
+ ```
148
+ no page JSON found under .../metadata/page — nothing to validate
149
+ ```
150
+
151
+ ```
152
+ Plugin Tests
153
+ ────────────────────────────────────────
154
+ ✓ plugin.json:valid-json
155
+ 1 passed, 0 failed, 0 semantic warning(s), 3 skipped
156
+ ```
157
+
158
+ That's a healthy empty plugin. Now add an [entity](./add-an-entity.md).
159
+
160
+ ## Common mistakes
161
+
162
+ | Symptom | Cause | Fix |
163
+ | --- | --- | --- |
164
+ | `erp menu create` route becomes `C:/Program Files/Git/...` | Git Bash rewrites the leading `/` | run it from PowerShell, or `MSYS_NO_PATHCONV=1` |
165
+ | you renamed `id` after building pages | id is in every page/i18n/menu key | pick the id once; if you must rename, sed the whole tree |
166
+ | publish rejected: role name already exists | another installed plugin has a role with the same `name` | make your role `name` distinctive |
167
+
168
+ ## What to read next
169
+
170
+ - [Add an entity](./add-an-entity.md)
171
+ - [Build a page](./build-a-page.md)
172
+ - [Validate and test a plugin](./validate-and-test.md)
@@ -0,0 +1,114 @@
1
+ ---
2
+ title: Expose a plugin operation as an AI tool
3
+ audience: tenant
4
+ ---
5
+
6
+ # Expose a plugin operation as an AI tool
7
+
8
+ ## What you're doing
9
+
10
+ This is different from [Use the MCP server with an AI agent](./use-the-mcp-server.md)
11
+ — that guide is about *your coding agent* (Claude Code, Cursor) getting
12
+ grounded while it writes your plugin. **This guide is about the ERP
13
+ product's own embedded AI assistant** calling *your plugin's* real
14
+ operations for an end user, at runtime, once your plugin is installed.
15
+
16
+ Declare which of your plugin's REST endpoints are safe for the AI to call,
17
+ under what permission, with what risk level. The platform reads this at
18
+ install time and registers real, callable tools — no separate admin step,
19
+ no retraining, no hand-written tool-registry entry.
20
+
21
+ | | Your plugin's REST APIs | Your declared AI tools |
22
+ | --- | --- | --- |
23
+ | Who calls it | Any authorized caller | The platform's AI assistant, on a user's behalf |
24
+ | Default | Every endpoint you write | **None** — opt-in only |
25
+ | Where it's declared | Your controller code | `metadata/tool/*.json` files |
26
+
27
+ **Never expose every endpoint automatically.** An internal cache-clear
28
+ endpoint or an admin-only bulk-delete has no business being AI-callable just
29
+ because it exists — only declare the operations you've deliberately decided
30
+ are safe for an AI to invoke on a user's behalf.
31
+
32
+ ## The file
33
+
34
+ One JSON file per tool, under your plugin's `metadata/tool/` directory —
35
+ same convention as `metadata/page/`, `metadata/form/`, `metadata/provider/`.
36
+ Real, shipped example — `hello-plugin`'s own
37
+ `metadata/tool/hello.greeting.get.json`:
38
+
39
+ ```json
40
+ {
41
+ "toolCode": "hello.greeting.get",
42
+ "name": "Get Hello Greeting",
43
+ "description": "Returns hello-plugin's demo greeting for the current tenant.",
44
+ "inputSchema": { "type": "object", "properties": {} },
45
+ "handlerType": "REST",
46
+ "handlerConfig": {
47
+ "path": "/api/v1/plugins/hello-plugin/greeting",
48
+ "method": "GET"
49
+ },
50
+ "requiredPermission": "HELLO_PLUGIN_DEMO",
51
+ "applicationCode": "hello-plugin",
52
+ "riskLevel": "LOW",
53
+ "strictPermissionCheck": false
54
+ }
55
+ ```
56
+
57
+ `handlerConfig` also accepts `pathParams`/`queryParams` (input keys
58
+ substituted into the path/query string) and `bodyFromInput: true` (the whole
59
+ input map sent as a POST body) — see `hcm-ai-intelligence`'s own 18 tool
60
+ files for real examples of each, including a real **cross-plugin** one
61
+ (`employee.search` calling `hcm-employee`'s own endpoint, not
62
+ `hcm-ai-intelligence`'s own namespace — a deliberately supported pattern for
63
+ an AI-assistant module that fronts several other plugins).
64
+
65
+ ## The namespace rule
66
+
67
+ `handlerConfig.path` **must** be under `/api/v1/plugins/**` (any plugin's own
68
+ namespace) or `/api/v1/entities/**` (the Entity Engine data API). A tool
69
+ pointed at an internal/admin/authoring route is rejected at install — before
70
+ your plugin touches anything else.
71
+
72
+ ## Ownership
73
+
74
+ Re-installing/upgrading your own plugin re-registers your own tools
75
+ idempotently. Declaring a `toolCode` another plugin already owns — or one
76
+ that was created manually outside any plugin — fails install with a clear
77
+ error; tool codes aren't a shared free-for-all namespace.
78
+
79
+ Disabling or uninstalling your plugin deactivates its tools (not a hard
80
+ delete) — re-enabling reactivates them.
81
+
82
+ ## Ground yourself first
83
+
84
+ ```bash
85
+ erp_get_schema {"name":"plugin-manifest"}
86
+ ```
87
+
88
+ Look at `hcm-ai-intelligence`'s real `metadata/tool/*.json` files for the
89
+ fullest working example of `pathParams`/`queryParams`/`bodyFromInput` in
90
+ practice, and for `riskLevel: "HIGH"` + `strictPermissionCheck: true` pairing
91
+ on a genuinely mutating operation.
92
+
93
+ ## How to verify it worked
94
+
95
+ Install your plugin, then check the discovery endpoint lists your tool:
96
+
97
+ ```bash
98
+ curl localhost:8080/api/v1/agents/metadata/tools \
99
+ -H "X-Tenant-Id: <id>" -H "X-Actor: <actor>" | grep your.tool.code
100
+ ```
101
+
102
+ ## Common mistakes
103
+
104
+ | Symptom | Cause | Fix |
105
+ | --- | --- | --- |
106
+ | Install fails: "not under an allowed namespace" | `handlerConfig.path` points outside `/api/v1/plugins/**` or `/api/v1/entities/**` | An AI tool can only call a real plugin business endpoint or the Entity Engine, never an internal/admin route |
107
+ | Install fails: tool already exists | `toolCode` collides with a manually-created tool or another plugin's tool | Pick a more specific, namespaced `toolCode` (e.g. `<your-plugin>.<noun>.<verb>`) |
108
+ | Tool still callable after uninstall | You're checking a cached list | Tools are deactivated, not deleted — re-query the discovery endpoint |
109
+ | AI never calls your tool | `requiredPermission` the calling user lacks, or `riskLevel`/policy gating it out | Check the user's actual permission grant first |
110
+
111
+ ## What to read next
112
+
113
+ - [Use the MCP server with an AI agent](./use-the-mcp-server.md) — the *other* MCP concept: grounding your coding agent while you build, not this guide's runtime AI-tool-calling
114
+ - [Create a plugin from scratch](./create-a-plugin.md)
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Guides
3
+ audience: tenant
4
+ ---
5
+
6
+ # Guides
7
+
8
+ Task-focused how-tos. Each page answers one question and follows the same rhythm:
9
+ what you're building → a complete, real, copy-pasteable example → a line-by-line
10
+ walkthrough → how to verify it worked → common mistakes → what to read next.
11
+ Every code sample is a real file under
12
+ [`tutorial/example-plugin/`](../tutorial/index.md) or
13
+ [`examples/`](../examples/), kept honest by
14
+ `node developer-docs/examples/test-examples.mjs`.
15
+
16
+ ## Getting started
17
+
18
+ - [Set up the SDK and connect to an environment](./set-up-the-sdk.md)
19
+ - [Create a plugin from scratch](./create-a-plugin.md)
20
+ - [Use the MCP server with an AI agent](./use-the-mcp-server.md)
21
+
22
+ ## Data
23
+
24
+ - [Add an entity](./add-an-entity.md)
25
+ - [Add a data provider, data view, or data service](./add-a-data-provider.md)
26
+ - [Seed config and reference data on install](./seed-data-on-install.md)
27
+
28
+ ## Screens
29
+
30
+ - [Build a page](./build-a-page.md)
31
+ - [Wire a page's data](./wire-a-pages-data.md)
32
+ - [Add a create / edit form](./add-a-create-edit-form.md)
33
+ - [Add a KPI or aggregation](./add-a-kpi.md)
34
+ - [Add menus](./add-menus.md)
35
+ - [Add translations (i18n)](./add-i18n.md)
36
+ - [Make a plugin work on desktop, tablet, and mobile](./responsive-plugin.md)
37
+
38
+ ## Logic
39
+
40
+ - [Add business rules and expressions](./add-business-rules.md)
41
+ - [Add an approval workflow](./add-an-approval-workflow.md)
42
+
43
+ ## Scheduled jobs (no Java)
44
+
45
+ - [Add a scheduled reminder job](./add-a-reminder-job.md)
46
+ - [Add a cross-row aggregation job](./add-an-aggregation-job.md)
47
+ - [Add a cadence-generator job](./add-a-cadence-job.md)
48
+ - [Add a compliance-sweep job](./add-a-compliance-job.md)
49
+
50
+ ## Ship it
51
+
52
+ - [Validate and test a plugin](./validate-and-test.md)
53
+ - [Publish and upgrade a plugin](./publish-and-upgrade.md)
@@ -0,0 +1,152 @@
1
+ ---
2
+ title: Publish and upgrade a plugin
3
+ audience: tenant
4
+ ---
5
+
6
+ # Publish and upgrade a plugin
7
+
8
+ ## What you're doing
9
+
10
+ Getting the `.spk` onto your ERP and, later, shipping a new version.
11
+
12
+ ## The complete sequence
13
+
14
+ ```bash
15
+ # 1. build the .spk (a zip of spk-assembly/)
16
+ erp plugin build spk-assembly -o office-equipment-1.0.0.spk
17
+
18
+ # 2. publish it to the current env, for a tenant
19
+ erp plugin publish office-equipment-1.0.0.spk --tenant 2
20
+ ```
21
+
22
+ Output on success:
23
+
24
+ ```
25
+ packaged 33 files -> office-equipment-1.0.0.spk (231986 bytes)
26
+ sha256: cef54cdf...
27
+ erp plugin publish: office-equipment-1.0.0.spk -> https://erp.example.com (env "dev", tenant 2)
28
+ POST .../api/v1/authoring/plugins/upload ...
29
+ installed:
30
+ {"pluginId":"office-equipment","version":"1.0.0","state":"installed","pf4jState":"STARTED", ...}
31
+ ```
32
+
33
+ `"state":"installed"` and `"pf4jState":"STARTED"` mean it's live. The server
34
+ recomputes the checksum itself — it never trusts the client digest.
35
+
36
+ ## What happens on install
37
+
38
+ 1. checksum verified server-side;
39
+ 2. the manifest is read (`id` and `version` are required; `mainClass` is
40
+ optional — a pure-JSON plugin leaves it `null`);
41
+ 3. PF4J loads the plugin — `lib/*.jar` if present, otherwise a built-in generic
42
+ no-op entry point for a pure-JSON plugin;
43
+ 4. entities are created / altered (idempotent-additive);
44
+ 5. providers, data views, data services, pages, menus, rules, workflows,
45
+ i18n and roles are installed;
46
+ 6. seed data is upserted;
47
+ 7. `AFTER_CREATE` register rules fire → scheduled jobs register.
48
+
49
+ If any step fails the whole install rolls back, including the PF4J load.
50
+
51
+ ## Version discipline — the rules that will bite you
52
+
53
+ ### 1. Bump the version before every publish
54
+
55
+ `plugin.json` `version` must increase. **A re-publish of a version that's
56
+ already installed is a silent no-op** — your fix does not ship. Bump it even if
57
+ you're certain the last publish already contained the change.
58
+
59
+ ### 2. A version is immutable once stored
60
+
61
+ Once the server has stored `office-equipment/1.0.0/office-equipment-1.0.0.spk`,
62
+ re-uploading **different bytes** under `1.0.0` is rejected:
63
+
64
+ ```
65
+ Refusing to overwrite immutable artifact at "system/plugins/.../1.0.0/..." with different content
66
+ ```
67
+
68
+ A version that genuinely **shipped** (reached `pf4jState: STARTED` on some
69
+ tenant) stays immutable forever — re-publishing different bytes under it is
70
+ rejected, go to the next version.
71
+
72
+ **But a *failed or partial* install is not a real release.** As of the SDK
73
+ publish-hardening pass, if a `1.0.0` publish never reached `STARTED` (it failed
74
+ later in the pipeline, or it's a metadata-only plugin that isn't PF4J-loaded),
75
+ re-publishing `1.0.0` **replaces** the stored `.spk` and retries — no version
76
+ bump needed just to fix a broken publish. `erp plugin publish` of an
77
+ already-installed id automatically takes the upgrade path, so this "edit a page,
78
+ re-publish the same version" loop works directly for JSON-only plugins.
79
+
80
+ ### 3. The platform cannot load two versions of the same plugin id at once
81
+
82
+ ```
83
+ There is an already loaded plugin ... with the same id (office-equipment) ...
84
+ Simultaneous loading of plugins with the same PluginId is not currently supported.
85
+ ```
86
+
87
+ The publish/upgrade path now **reliably stops and unloads the currently-loaded
88
+ same-id plugin before loading the new bundle** (stop → unload → GC hint → load →
89
+ start), so a normal in-place re-publish never hits this. If an earlier crash or a
90
+ transient DB error during uninstall left the old code *wedged* — `state:
91
+ uninstalled` in the install row but `pf4jState: STARTED` in the process — recover
92
+ without a backend restart:
93
+
94
+ ```bash
95
+ erp plugin force-unload office-equipment # clears the stuck PF4J load
96
+ erp plugin publish office-equipment-1.0.1.spk --tenant 2
97
+ ```
98
+
99
+ `erp plugin publish --force` does the force-unload automatically before
100
+ retrying, and `spark publish` / `erp plugin publish` also auto-retry once with a
101
+ force-unload if they see the "already loaded" error.
102
+
103
+ > These behaviours were all hit while building the tutorial module. Treat
104
+ > version numbers as cheap and monotonic; use `force-unload` rather than a
105
+ > restart when a load gets stuck.
106
+
107
+ ## Roles and names must be unique across installed plugins
108
+
109
+ A `plugin.json` role whose `name` **or** `code` collides with a role from
110
+ another installed plugin fails the install:
111
+
112
+ ```
113
+ A role named "Equipment Manager" already exists
114
+ Role "..." (code "...") already exists owned by "..."
115
+ ```
116
+
117
+ Prefix your role names and codes with something plugin-specific.
118
+
119
+ ## Upgrading
120
+
121
+ ```bash
122
+ # edit files, bump plugin.json version to 1.0.1, rebuild the lib jar if Java changed
123
+ erp plugin build spk-assembly -o office-equipment-1.0.1.spk
124
+ erp plugin publish office-equipment-1.0.1.spk --tenant 2
125
+ ```
126
+
127
+ Entities gain new columns; pages, rules, seed data are re-installed (upserted).
128
+ Nothing is dropped — a removed field's column stays, a removed seed row stays.
129
+
130
+ ## How to verify it worked
131
+
132
+ ```bash
133
+ erp plugin list
134
+ ```
135
+
136
+ Find your plugin: `"version": "1.0.1"`, `"state": "installed"`,
137
+ `"pf4jState": "STARTED"`. Then hit a page or an entity endpoint to confirm the
138
+ new content is live.
139
+
140
+ ## Common mistakes
141
+
142
+ | Symptom | Cause | Fix |
143
+ | --- | --- | --- |
144
+ | "fix didn't ship" | didn't bump `version` on a **shipped** version | bump; re-publish. (A *failed* same-version publish now retries in place.) |
145
+ | `Refusing to overwrite immutable artifact` | retrying a version that already reached `STARTED` on some tenant | go to the next version |
146
+ | `already loaded plugin ... same id` | a load wedged after a crash / DB blip | `erp plugin force-unload <id>` then re-publish (or `publish --force`) |
147
+ | role name/code clash | another plugin owns that role | prefix yours |
148
+
149
+ ## What to read next
150
+
151
+ - [Validate and test a plugin](./validate-and-test.md)
152
+ - Troubleshooting: [publishing and install errors](../troubleshooting/publishing-errors.md)