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: 6 additions & 0 deletions .ls-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ ignore:
# its own right (a stray leftover here is a review miss, not a
# shipped state).
- docs-drafts
# examples/ (goal 0249, ADR-0001 extension): shippable example
# content a user copies OUT of the repo -- the first resident is
# examples/plugins/mill-bookmark, the runtime-plugin platform's own
# reference plugin (copy the folder into the app's plugins directory
# and reload). Plain web-file naming inside, not the root rule.
- examples
- build
- frontend
- bin
Expand Down
88 changes: 88 additions & 0 deletions examples/plugins/mill-bookmark/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Bookmark -- Mill's reference runtime plugin (docs/goals/0249).
// Plain ESM, no build step: copy this folder into the app's plugins
// directory (Settings > Extensions > Open plugins folder) and reload.
//
// It contributes one canvas object on the object contract: a web
// address pinned to the board (source: url). The URL is edited right
// on the face (editRoute: inline); Open never touches the browser
// itself -- it asks Mill for the guarded open-url action, which the
// owner's guardrail rules evaluate per use.

export function activate(api) {
api.registerCanvasObject({
kind: 'bookmark',
label: 'Bookmark',
description: 'A web address pinned to the board.',
icon: '🔖',
source: 'url',
editRoute: 'inline',
defaultPayload: { url: '', title: '' },
renderFace(el, ctx) {
// Rebuild the face from the object's current data. All text
// lands via textContent/value -- never markup -- so a URL can
// never inject anything.
el.replaceChildren()
el.style.cssText = 'display:flex;flex-direction:column;gap:6px;padding:10px 12px;font:12px system-ui;height:100%;box-sizing:border-box'

const title = document.createElement('div')
title.style.cssText = 'display:flex;align-items:center;gap:6px;font-weight:600'
const glyph = document.createElement('span')
glyph.textContent = '🔖'
const titleText = document.createElement('span')
titleText.textContent = ctx.object.Payload.title || 'Bookmark'
title.append(glyph, titleText)

const input = document.createElement('input')
input.type = 'text'
input.placeholder = 'https://…'
input.value = ctx.object.Payload.url || ''
input.setAttribute('data-testid', 'bookmark-url-input')
input.style.cssText = 'font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d0d7de;border-radius:6px;width:100%;box-sizing:border-box'
// Commit on Enter/blur, not per keystroke -- each payload
// write re-renders this face, which would rebuild the input
// under the caret mid-word.
const commit = () => {
const next = input.value.trim()
if (next === (ctx.object.Payload.url || '')) return
void ctx.updatePayload({ url: next, title: next ? new URL(withScheme(next)).hostname : '' }).catch(() => {
status.textContent = 'Could not save the address.'
})
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); commit() }
e.stopPropagation() // board shortcuts stay out of typing
})
input.addEventListener('blur', commit)

const row = document.createElement('div')
row.style.cssText = 'display:flex;align-items:center;gap:8px'
const open = document.createElement('button')
open.type = 'button'
open.textContent = 'Open'
open.setAttribute('data-testid', 'bookmark-open')
open.style.cssText = 'font:11px system-ui;padding:3px 10px;border:1px solid #d0d7de;border-radius:6px;background:#f6f8fa;cursor:pointer'
const status = document.createElement('span')
status.setAttribute('data-testid', 'bookmark-status')
status.style.cssText = 'font:11px system-ui;color:#57606a'
open.addEventListener('click', async () => {
const url = withScheme((ctx.object.Payload.url || '').trim())
if (!url) { status.textContent = 'Enter an address first.'; return }
status.textContent = 'Asking…'
try {
const result = await ctx.requestGuardedAction('open-url', { url }, `Open ${url} in the browser`)
status.textContent = result.approved ? 'Opened.' : 'Not allowed' + (result.ruleLabel ? ` (${result.ruleLabel}).` : '.')
} catch (err) {
status.textContent = String(err && err.message ? err.message : err)
}
})
row.append(open, status)

el.append(title, input, row)
},
})
}

function withScheme(url) {
if (!url) return url
return /^https?:\/\//.test(url) ? url : 'https://' + url
}
9 changes: 9 additions & 0 deletions examples/plugins/mill-bookmark/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "mill-bookmark",
"name": "Bookmark",
"version": "1.0.0",
"description": "Keeps a web address on the board and opens it in your browser.",
"author": "Mill examples",
"minMillVersion": "0.9.0",
"capabilities": ["open-url"]
}
7 changes: 7 additions & 0 deletions frontend/.dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ module.exports = {
from: { path: '^src/atlas/extensions' },
to: { path: '^(src/shared/bindings\\.ts|bindings/.*/internal/services/)' },
},
{
name: 'plugin-sdk-imports-nothing',
severity: 'error',
comment: 'ADR-0047 / goal 0249: src/plugins/sdk.ts describes exactly what an out-of-tree plugin sees, and a plugin receives capabilities only through the api object handed to activate() -- never through an import. The SDK module therefore imports NOTHING (kernel, bindings, or otherwise); host-side plumbing lives in src/plugins/{hostApi,loader,PluginFaceContent} which legitimately reach the kernel.',
from: { path: '^src/plugins/sdk\\.ts$' },
to: {},
},
],
options: {
doNotFollow: { path: 'node_modules' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export {

export type {
Rule,
Step,
Verdict
} from "./models.js";
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ export interface Rule {
"Seed": seedorigin$0.Origin;
}

/**
* Step is what one about-to-execute step looks like to the evaluator.
*/
export interface Step {
"NodeTypeID": string;

/**
* RequestID is the step's configured HTTPRequest reference, if any
* (an integration-http node's requestId config).
*/
"RequestID": string;
"WorkflowID": string;
"NodeID": string;

/**
* Env is the condition-evaluation environment: Payload, Attributes,
* Config -- same shape Decision-edge conditions already evaluate
* against, so rule authors learn one expression surface.
*/
"Env": { [_ in string]?: any } | null;
}

/**
* Verdict is one evaluation's outcome: the effect plus which rule
* produced it (nil RuleID/RuleLabel means the effect-class default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,19 @@ export function SetAtlasSession(state: $models.AtlasSessionState): $CancellableP
return $Call.ByID(1784937459, state);
}

/**
* SetBoardObjectPayload merges patch into a board object's Payload --
* the content-plane write door for a payload-carrying object whose
* data changes after placement (docs/goals/0249: a plugin object's own
* fields, written host-mediated so plugin code never touches a
* binding). A key with an empty value deletes that key; every other
* key overwrites. mirrorPath changes re-arm the file watch the same
* way creation does.
*/
export function SetBoardObjectPayload(id: string, patch: { [_ in string]?: string } | null): $CancellablePromise<atlas$0.BoardObject> {
return $Call.ByID(1717906545, id, patch);
}

/**
* SetBoardObjectPosition updates a board object's placement within its
* parent's canvas -- the same drag-persistence call cards/notes go
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* one shared entity string rather than one per family, since they all
* persist as a single blob (atlassvc's own atlasStateKey) and a
* change to any of them means the whole surface should refresh.
* "extension" carries the canvas-extension id whose enabled/disabled
* state just changed (Settings > Extensions).
*/
export interface Changed {
"entity": string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ export function DeleteRule(id: string): $CancellablePromise<void> {
return $Call.ByID(1475597571, id);
}

/**
* EvaluateAction adapts a generic action's kind/attributes into a Step
* and evaluates it through EvaluateStep. kind fills Step.NodeTypeID --
* the same scope axis a workflow node's own NodeTypeID already targets
* -- so a rule authored against a NodeTypeID scope also targets a
* guarded action of that kind, by construction, with no separate rule
* vocabulary to maintain.
*/
export function EvaluateAction(kind: string, attributes: { [_ in string]?: string } | null, $class: guardrail$0.EffectClass): $CancellablePromise<guardrail$0.Verdict> {
return $Call.ByID(4024562939, kind, attributes, $class);
}

/**
* EvaluateStep is the guardrail's rule-evaluation core: judges a
* fully-formed Step against the current rules with guardrail.Evaluate's
* deny > ask > allow > class-default precedence. A thin wrapper by
* design -- the extraction this pays for is a single call site every
* caller (a workflow step, a generic action) shares, so they can never
* silently diverge into two different evaluations of the same rules.
*/
export function EvaluateStep(step: guardrail$0.Step, $class: guardrail$0.EffectClass): $CancellablePromise<guardrail$0.Verdict> {
return $Call.ByID(1285652131, step, $class);
}

/**
* PendingGuardedActions is the Wails-bound listing the Review queue
* renders -- the same records RequestGuardedAction parks.
*/
export function PendingGuardedActions(): $CancellablePromise<$models.PendingGuardedAction[] | null> {
return $Call.ByID(4230330282);
}

/**
* ResolveGuardedAction is the Review queue's approve/deny door for a
* parked guarded action (docs/goals/0249 closed the render-alongside
* half this park always promised): the blocked RequestGuardedAction
* caller wakes with the human's answer.
*/
export function ResolveGuardedAction(id: string, approve: boolean): $CancellablePromise<void> {
return $Call.ByID(3175233248, id, approve);
}

/**
* Rules returns every stored rule.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export {
};

export type {
PendingGuardedAction,
RuleTestResult
} from "./models.js";
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

/**
* PendingGuardedAction is a parked, not-yet-resolved GuardedAction --
* the non-workflow analogue of executionsvc.PendingApproval (docs/adr/0047
* §5 point 3): additive, never reshaping the workflow park it is meant
* to one day render alongside.
*/
export interface PendingGuardedAction {
"ID": string;
"Kind": string;
"Attributes": { [_ in string]?: string } | null;
"Description": string;
"Source": string;
"CreatedAt": string;
}

/**
* RuleTestResult is one dry-run's outcome for the Configure tester.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ export interface MCPWriteActivity {
/**
* MCPWriteRequest is the frontend-facing shape for a still-PENDING
* write (the "mcp-write-approval" event payload and PendingMCPWrites'
* own return type) -- narrower than MCPWriteRecord (no ToolName/
* ArgsJSON/executor internals), same field names the banner/Review UI
* already bind against.
* own return type) -- narrower than the shared store's own
* GuardedActionRecord (no ToolName/ArgsJSON/executor internals), same
* field names the banner/Review UI already bind against.
*/
export interface MCPWriteRequest {
"id": string;
"description": string;
"createdAt": string;

/**
* LastPolledAt mirrors MCPWriteRecord's own field (docs/goals/0026
* LastPolledAt mirrors the shared record's own field (docs/goals/0026
* item 3) -- nil when the requester has never called
* check_write_status on this id yet.
*/
Expand All @@ -59,8 +59,8 @@ export interface MCPWriteRequest {
* MCPWriteResolved is the frontend-facing shape for an already-resolved
* write (docs/goals/0026 item 6) -- Review's Recently-resolved section
* reads this alongside RunSummary's own resolved rows, merged
* newest-first. Retained for the same 24h window check_write_status
* already promises (sweepLocked's own retention) -- "durable across a
* newest-first. Retained for the same retention window check_write_status
* already promises (the shared store's own sweep) -- "durable across a
* restart" and "still visible for the same window an MCP client can
* still poll" are the same guarantee, not two.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

import * as PluginService from "./pluginservice.js";
export {
PluginService
};

export type {
GuardedActionDecision,
Manifest,
PluginInfo
} from "./models.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

/**
* GuardedActionDecision is RequestGuardedAction's wire shape.
*/
export interface GuardedActionDecision {
"Approved": boolean;
"Effect": string;
"RuleLabel": string;

/**
* Performed is true when Mill executed the approved action itself
* (the plugin never receives the primitive).
*/
"Performed": boolean;
}

/**
* Manifest is the converged plugin manifest shape (docs/adr/0047 §1:
* identity metadata + a declared capability set; contributions happen
* at activate() time through the host API, so they are not restated
* here).
*/
export interface Manifest {
"id": string;
"name": string;
"version": string;
"description": string;
"author": string;
"minMillVersion": string;
"capabilities": string[] | null;
}

/**
* PluginInfo is one scanned plugin as the Extensions surface and the
* loader see it. Error is a load-blocking validation problem stated
* for the human (the row renders it; the loader skips the plugin) --
* a plugin is either fully valid or visibly broken, never silently
* half-loaded.
*/
export interface PluginInfo {
"Manifest": Manifest;
"Dir": string;
"Error": string;
}
Loading
Loading