Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Moorline Kit

This repository owns `@moorline/package-kit`, the package authoring SDK and CLI for third-party Moorline package builders.
This repository owns `@moorline/package-kit`, the package authoring SDK and CLI for Moorline package builders.

The host runtime lives in `Moorline/moorline`. Official installable packages live in `Moorline/packages`.

Moorline packages extend an operator-controlled runtime. Package authors can build external surface adapters, providers, plugins, skills, and bundles that participate in durable event/work orchestration. Chat is one supported transport shape, not the architectural center.

## Development

The public development path consumes the published `@moorline/contracts` package:
Expand Down Expand Up @@ -37,7 +39,7 @@ bun run test:fast
bun run build
```

Package authoring documentation lives in `docs/PACKAGE_AUTHORING.md`.
Package authoring documentation lives in `docs/PACKAGE_AUTHORING.md`. Shared wording lives in `docs/TERMINOLOGY.md`.

## Releases

Expand Down
167 changes: 158 additions & 9 deletions docs/PACKAGE_AUTHORING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package Authoring

This guide is for third-party developers who want to create Moorline packages for other users.
This guide is for developers who want to create Moorline packages for other users or teams.

Canonical naming:
- [TERMINOLOGY.md](./TERMINOLOGY.md)
Expand Down Expand Up @@ -44,6 +44,7 @@ Important:
- you do not need the Moorline repo locally
- third-party authors should use `npx @moorline/package-kit`
- users should install finished bundles, not raw source trees
- packages run inside the operator-controlled Moorline runtime once activated

## Two Required Files

Expand Down Expand Up @@ -154,10 +155,15 @@ Use it when you want to integrate:
- Discord
- Slack
- email
- a custom chat surface
- GitHub or issue trackers
- CI systems
- incident tools
- a custom external surface

Transport packages occupy the core `transport` activation key. Moorline activates at most one package for that key; packages that need to bridge multiple external surfaces should expose that multiplexing inside a single transport package.

Transports can emit chat-like messages, native actions, resource lifecycle events, or generic external events. Use `external.event.received` when an outside system reports something that is not naturally a chat message, such as `issues.opened`, `workflow.failed`, `incident.triggered`, or `email.received`.

### Plugin

A plugin package extends runtime behavior.
Expand All @@ -171,13 +177,13 @@ Use it for:
- renderers
- integrations

Plugins are trusted local runtime code. Moorline validates plugin manifests, declared capabilities, package layout, and install safety, but it does not sandbox plugin JavaScript after the package is activated. Install third-party plugins only from sources you are willing to let run inside the local Moorline process.
Plugins are trusted runtime code. Moorline validates plugin manifests, declared capabilities, package layout, and install safety, but it does not sandbox plugin JavaScript after the package is activated. Install third-party plugins only from sources you are willing to let run inside the operator-controlled Moorline process.

Plugins can be activated or deactivated independently. Most plugins do not need an activation key, so many can be activated together.

#### Package State And Jobs
#### Package State, Jobs, And Work

Plugins that need durable local state should use package state through the runtime context instead of asking the host for a feature-specific table. Declare:
Plugins that need durable package state should use package state through the runtime context instead of asking the host for a feature-specific table. Declare:

```json
{
Expand Down Expand Up @@ -234,7 +240,130 @@ export default {
await context.cancelPackageJob('sync:primary');
```

Package jobs are for package-owned behavior. If a feature is specific to one package, model it with package state and package jobs rather than adding host-owned concepts.
Package jobs are for recurring package-owned behavior. They are timers that dispatch package actions.

Plugins that need durable event-driven work should use package work items instead. Work items are runtime-owned queue records with idempotency keys, attempts, leases, retry/dead-letter states, optional external resources, and optional session bindings. Declare:

```json
{
"capabilities": ["package.work.manage"]
}
```

Use work items for webhook retries, polling dedupe, CI failure repair, issue processing, inbox triage, or any workflow where work must survive runtime restarts:

```js
await context.enqueueWorkItem({
queue: 'github-issues',
idempotencyKey: 'github:acme/repo:issue:123',
externalResource: {
provider: 'github',
kind: 'issue',
id: 'acme/repo#123',
url: 'https://github.com/acme/repo/issues/123',
title: 'Improve setup'
},
payload: { action: 'opened' }
});

const item = await context.claimWorkItem({
queue: 'github-issues',
leaseSeconds: 300
});

if (item) {
try {
const created = await context.createSession({
requestedName: 'Issue 123',
runtimeMode: 'full-access',
objective: 'Investigate and prepare a fix for issue #123.',
externalResource: item.externalResource,
workItemId: item.workItemId
});
await context.completeWorkItem({ workItemId: item.workItemId, phase: 'session-created' });
console.log(created.session.sessionId);
} catch (error) {
await context.failWorkItem({
workItemId: item.workItemId,
error: error instanceof Error ? error.message : String(error),
retryAfter: new Date(Date.now() + 60_000).toISOString()
});
}
}
```

Use package state for package-specific configuration or indexes, package jobs for schedules, and work items for durable queue/retry/recovery. Do not add host concepts for package-specific data unless multiple package families need the same host-owned primitive.

#### External Events

Plugins can declare and implement `onExternalEvent` to receive generic external events from transports:

```json
{
"hooks": ["onExternalEvent"],
"capabilities": ["package.work.manage"]
}
```

```js
export default {
manifest,
async onExternalEvent(event, context) {
if (event.source !== 'github' || event.eventName !== 'issues.opened') {
return { handled: false, continueDispatch: true };
}
await context.enqueueWorkItem({
queue: 'github-issues',
idempotencyKey: event.idempotencyKey,
externalResource: event.resource,
payload: { eventName: event.eventName, payload: event.payload }
});
return { handled: true };
}
};
```

Use `onTransportEvent` for broad transport observation or chat-style message routing. Use `onExternalEvent` when the package handles non-chat events as workflow triggers.

#### Gates And Headless Runs

Plugins that need deterministic checks can run runtime gates:

```js
const gate = await context.runGate({
gateId: 'lint',
command: 'bun',
args: ['run', 'lint'],
required: true,
workItemId: item.workItemId,
sessionId: item.sessionId
});
```

Plugins that need a one-shot provider assessment can use session-backed headless runs. Declare `provider.headless.run`:

```json
{
"capabilities": ["provider.headless.run"]
}
```

```js
const result = await context.runHeadless({
requestedName: 'Classify issue',
runtimeMode: 'read-only',
prompt: 'Return JSON with { "ready": boolean, "reason": string }.',
outputSchema: {
type: 'object',
required: ['ready', 'reason'],
properties: {
ready: { type: 'boolean' },
reason: { type: 'string' }
}
},
requireStructuredOutput: true
});
```

### Skill

Expand Down Expand Up @@ -393,6 +522,26 @@ Example:
}
```

External event worker example:

```json
{
"id": "acme/github-worker",
"name": "acme/github-worker",
"version": "0.0.1",
"type": "plugin",
"description": "Turns GitHub issue events into durable Moorline work.",
"entrypoint": "index.mjs",
"capabilities": [
"package.work.manage",
"session.create",
"session.direct",
"provider.headless.run"
],
"hooks": ["onExternalEvent"]
}
```

### Skill manifest

Required:
Expand Down Expand Up @@ -593,13 +742,13 @@ npx @moorline/package-kit validate ./dist/moorline-bundle.tar.gz
Default validation is structural only.
It does not execute your package code.

If you want a trusted local runtime import smoke test too:
If you want a trusted runtime import smoke test too:

```bash
npx @moorline/package-kit validate ./dist/moorline-bundle --runtime-smoke
```

The runtime smoke test is still a trust decision, not a sandbox. Run it only for packages you are prepared to execute locally.
The runtime smoke test is still a trust decision, not a sandbox. Run it only for packages you are prepared to execute in your current environment.

Inspect the resolved package metadata:

Expand All @@ -609,7 +758,7 @@ npx @moorline/package-kit inspect ./dist/moorline-bundle

## How To Test Locally

Install the directory bundle into your local Moorline runtime.
Install the directory bundle into a Moorline runtime you control.

Example:

Expand Down
31 changes: 31 additions & 0 deletions docs/TERMINOLOGY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Terminology

Use these terms consistently in package authoring docs and package metadata.

## Preferred Terms

- `operator-controlled runtime`
- The person or team running Moorline controls the runtime boundary: package activation, provider/transport selection, policy, state, audit, and deployment environment.
- `external surface`
- A system Moorline connects to through a transport or adapter. Examples include Discord, Slack, GitHub, email, CI, incident tools, and custom APIs.
- `transport`
- An installable package that connects Moorline to an external surface and emits runtime transport events. A transport may be chat-like, but it does not have to be chat.
- `provider`
- An installable package that executes agent work.
- `plugin`
- Trusted runtime code that adds behavior, hooks, tools, commands, workflow logic, or integrations.
- `external resource`
- A normalized reference to the outside object that caused or receives work, such as a GitHub issue, CI run, email thread, incident, or ticket.
- `work item`
- Runtime-owned durable package work with status, attempts, idempotency, leases, and optional session/resource binding.
- `package job`
- Package-owned scheduled action dispatch. Use this for recurring timers. Use work items for durable queue/retry/recovery around event-driven work.

## Avoid As Product Identity

- `local-first`
- Prefer `operator-controlled`, `self-hostable`, or concrete deployment wording. Local execution is supported, but Moorline is not defined by laptop-only operation.
- `chat-centered`
- Prefer `event/work orchestration`, `external surface`, or the specific transport name.
- `local runtime code`
- Prefer `trusted runtime code` or `operator-controlled runtime code`.
68 changes: 68 additions & 0 deletions tests/unit/package-kit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,74 @@ describe('@moorline/package-kit', () => {
expect(existsSync(markerPath)).toBe(true);
});

it('accepts external event worker plugin manifests structurally', async () => {
const bundleDir = createTempRoot('moorline-package-kit-external-worker-');
writeFileSync(
join(bundleDir, 'manifest.json'),
JSON.stringify(
{
id: 'acme/github-worker',
name: 'acme/github-worker',
version: '0.0.1',
type: 'plugin',
description: 'Turns GitHub issue events into durable Moorline work.',
entrypoint: 'index.mjs',
capabilities: [
'package.work.manage',
'session.create',
'session.direct',
'provider.headless.run'
],
hooks: ['onExternalEvent']
},
null,
2
),
'utf8'
);
writeFileSync(
join(bundleDir, 'moorline.dist.json'),
JSON.stringify(
{
schemaVersion: 1,
name: 'GitHub Worker',
description: 'Turns GitHub issue events into durable Moorline work.',
version: '0.0.1'
},
null,
2
),
'utf8'
);
writeFileSync(
join(bundleDir, 'index.mjs'),
[
"import manifest from './manifest.json' with { type: 'json' };",
'export default {',
' id: manifest.id,',
' manifest,',
' async onExternalEvent(event, context) {',
' await context.enqueueWorkItem({',
" queue: 'github-issues',",
' idempotencyKey: event.idempotencyKey,',
' externalResource: event.resource,',
' payload: { eventName: event.eventName, payload: event.payload }',
' });',
' return { handled: true };',
' }',
'};'
].join('\n'),
'utf8'
);

const validated = await validatePackagePath({ path: bundleDir, surface: 'plugin' });
expect(validated.manifest).toMatchObject({
id: 'acme/github-worker',
capabilities: ['package.work.manage', 'session.create', 'session.direct', 'provider.headless.run'],
hooks: ['onExternalEvent']
});
});

it('rejects archive validation when tar entries attempt path traversal', async () => {
const root = createTempRoot('moorline-package-kit-traversal-');
const bundleDir = join(root, 'bundle');
Expand Down
Loading