Skip to content

feat: add skill scripts runtime#572

Draft
anfibiacreativa wants to merge 31 commits into
mainfrom
feat/skill-scripts-runtime
Draft

feat: add skill scripts runtime#572
anfibiacreativa wants to merge 31 commits into
mainfrom
feat/skill-scripts-runtime

Conversation

@anfibiacreativa

Copy link
Copy Markdown
Member

THIS CODE IS NOT MEANT TO STAY PERMANENTLY. IT'S AN ITERIM SOLUTION FOR CODE EXECUTION.

Summary

  • Adds a worker substrate that executes skill scripts triggered by the da-agent orchestration tool (PR 1).
  • Wires up marketplace resolution + host.deps injection so scripts can call DA APIs without bundling credentials.
  • Converts attachmentRef to bytes for binary-capable skill steps.

Why

Skills need a sandboxed execution environment inside da-nx so scripts fetched from the marketplace (see da-agent PR 1) can run safely and interact with the document. This PR is co-dependent with da-agent PR 1 — ship them together.

The demo marketplace source (exp-workspace/skills, ew/ tree) is hardcoded for now and should move to the config sheet (targeting adobe/skills) for prod. See the skill-script-runtime migration plan.

What Changed

  • Worker substrate: sandboxed script execution (localStorage neutered, document access gated)
  • Agent-triggered round-trip: da-agent skill_run_script → da-nx worker → result back to agent
  • Marketplace resolution: worker resolves skill body via provider URL, caches per session
  • host.deps injection: injects authenticated DA API helpers into skill script scope
  • attachmentRef → bytes: binary payload conversion for attachment-capable skills
  • Chat empty-directive render fix: blank directive no longer renders as broken UI element
  • .docx accept: file picker now accepts .docx uploads
  • Design docs: worker substrate architecture and sandbox isolation notes

Test Plan

  • npm test passes (all relevant test suites)
  • Security suite: sandbox isolation — localStorage inaccessible from script scope; document write blocked outside allowed surface; no credential bleed into script scope
  • Agent round-trip: send a chat message that invokes a marketplace skill end-to-end; script executes and returns structured result
  • host.deps injection: skill script can call a DA API helper without holding its own credentials
  • attachmentRef → bytes: upload a file attachment; skill receives binary payload correctly
  • Chat empty-directive fix: send a message with no directive; no broken UI element appears
  • .docx accept: file picker shows .docx as an accepted type

Risks / Follow-ups

  • Demo marketplace source exp-workspace/skills (ew/) is hardcoded — must move to config sheet targeting adobe/skills before prod
  • Co-dependent with da-agent PR 1; do not ship independently
  • Sandbox isolation is enforced in the worker; review surface area before widening host.deps

Reference

PR 2 of 4 — skill-script marketplace + runtime.
Co-dependent pair: da-agent (PR 1) ↔ da-nx (PR 2) — ship together.
ew-extensions (PR 3) and ew-devs-cli (PR 4) are standalone.

Test URLs

URL
Before https://main--da-nx--adobe.aem.page/
After https://feat-skill-scripts-runtime--da-nx--adobe.aem.page/

anfibiacreativa and others added 29 commits June 27, 2026 10:59
- capabilities.js: NETWORK/SECRETS/PII/STORAGE constants + isClientEligible
  (empty capabilities array = client-eligible, enforced by construction)
- worker-host.js: WORKER_BOOTSTRAP string — neuters fetch, XHR, WebSocket,
  importScripts, indexedDB, caches, Notification, navigator.sendBeacon before
  loading any skill module; exposes only a buffered host.log to the script
- runner.js: runSkillScript({ manifest, moduleUrl, input }) — eligibility gate
  returns { error: 'requires server runtime' } for non-empty capabilities;
  otherwise spins a blob-URL module worker, races against timeoutMs, returns
  { json: output } or { error }; SANDBOX runner seam commented at the strategy
  point for a future server-side runner with identical caller shape
- index.js: public surface re-export
- fflate dep: nx2/deps/fflate/ (src + minified dist, nx2:build:fflate script)
- docx-to-markdown built-in skill: script.js + manifest.js under
  nx2/blocks/chat/skills-builtin/docx-to-markdown/; pure ECMAScript + lazy
  fflate; extracts w:t nodes across document/header/footer XML, unescapes
  XML entities, returns { markdown }; capabilities: [] (no ambient access)
- 9 tests: eligibility gate, server-runtime gate, pure worker execution,
  ambient-global neutering (fetch undefined in worker), timeout, docx
  round-trip proof, entity unescape, corrupt-input → { error }

JSON-serializable I/O contract designed for portability to a future server/AO
sandbox runner — contract is language-neutral, Python mirror is def entry(input, host).

Co-Authored-By: Claude <noreply@anthropic.com>
- add §5.2.1 agent-triggered orchestration sequence diagram
- document two trigger modes (client-triggered vs agent-triggered)
- note the SANDBOX seam and data-flow trade-offs in §5.3
…cript round-trip

- add nx2/blocks/chat/utils/skill-script-loader.js: trusted client-side manifest
  resolver; fetches .da/skills/<id>/skill.md, parses flat execution_* frontmatter
  into { entry, runtimes, capabilities, timeoutMs }, resolves script.js module URL.
  Eligibility comes from here — never from the agent's tool args.
- wire _onToolEvent in chat-controller.js to intercept skill_run_script TOOL_CALL:
  resolves manifest via skill-script-loader, runs isClientEligible from the trusted
  manifest (capability hints in agent args are ignored), dispatches to runSkillScript,
  records the result with the existing virtual-message pattern (_recordSkillResult),
  and re-engages the agent via _stream so it can continue reasoning with the output.
- add _recordSkillResult helper on ChatController to encapsulate virtual-message
  recording + tool-card state update for skill results.
- tests: skill-script-loader (frontmatter parsing, empty capabilities → [], module URL
  resolution, error paths) and chat-controller round-trip (happy path, server-runtime
  gate, resolve error, security — agent capability hint ignored, virtual-message
  expansion via _messagesForAgent).
Add skill.md with flat execution_* frontmatter alongside the existing script.js.
The manifest declares execution_capabilities as empty (client-eligible pure compute).
This file is the seed artifact for .da/skills/docx-to-markdown/ and expresses the
full skill contract (input { bytesBase64 }, output { markdown }, timeout 5000ms).
…cution

Three E2E tests in test/nx2/blocks/chat/skill-script-e2e.test.js prove the full
client path without mocking the substrate:

- Happy path: real sandboxed Web Worker loads the real docx-to-markdown script.js
  (served by WTR at localhost), receives a real fflate-built .docx fixture, and
  returns markdown containing "hello e2e"; _messagesForAgent() expands the virtual
  message into the correct ASSISTANT/TOOL pair.
- Eligibility gate: execution_capabilities:network in the served skill.md yields
  TOOL_STATE.ERROR with "requires server runtime" before any worker is created.
- Security: agent-supplied capabilities:[] hint in tool args is ignored; the
  trusted manifest capabilities decide eligibility.

Only three things are simulated: fetch for skill.md (real bytes, no DA Admin
needed), _stream (immediate resolve, no live LLM), and the moduleUrl redirect from
DA Admin to localhost (DA_ADMIN is a closed-over constant unreachable via fetch stub).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s from allowlist

- skills/marketplace code now lives at <skillId>/scripts/<entry>.js (not flat
  script.js); resolveSkill builds the URL from execution_entry + runtime ext
- parseSkillFrontmatter parses execution_dependencies (comma-separated) into
  dependencies: string[] on the manifest, flowing through resolveSkill → runner
- worker-host exports DEPENDENCY_ALLOWLIST (name → vetted absolute URL); runner
  resolves paths to absolute before postMessage so blob-URL workers can import()
- worker bootstrap loads each declared dep via import(allowlist[name]) into
  host.deps[name]; refuses with { error: 'dependency "..." not allowed' } for
  any dep not in the allowlist — no arbitrary import permitted
- scripts/convert.js uses host.deps.fflate instead of importing /nx2/deps/...
  directly — skill is now host-path-independent
- manifest.js adds dependencies: ['fflate']; skill.md adds execution_dependencies
- GH marketplace rework preserved (blob URL for MIME-type correction, no org/site)
- tests: loader tests cover scripts/ URL and execution_dependencies parsing;
  runtime tests cover allowlist injection and non-allowlisted refusal;
  E2E real-worker test runs real scripts/convert.js with injected fflate

Co-Authored-By: Claude <noreply@anthropic.com>
MARKETPLACE_RAW_BASE was missing the /ew path segment, so all skill.md
and script fetches resolved to the wrong URL. Updated constant and
matching test assertion to include /ew/.

Co-Authored-By: Claude <noreply@anthropic.com>
…tay client-side)

- in the skill_run_script branch of _onToolEvent, if input.attachmentRef is set,
  look up the attachment in this._pendingAttachments by id
- if found: build effectiveInput by injecting bytesBase64 (from dataBase64), fileName,
  mediaType, and stripping attachmentRef; other skillInput fields co-exist unchanged
- if not found: record { error: 'attachment <ref> not found' } and bail without running
- if no attachmentRef: pass skillInput through unchanged (param-only skills unaffected)
- bytes are resolved entirely from the local _pendingAttachments array; agent args
  never supply bytes — the security invariant is preserved by construction
- add roundtrip tests: missing attachment → error, no attachmentRef → passthrough
- add E2E test: real worker receives resolved bytesBase64 via attachmentRef and the
  docx fixture converts to markdown containing the expected text
…, marketplace-only

- extend ambient-neutering tests to full §10 network set: XMLHttpRequest,
  WebSocket, importScripts, navigator.sendBeacon
- assert exfiltration blocked: fetch() call errors rather than completing
- assert storage globals absent: indexedDB, caches, localStorage
- assert document and document.cookie unavailable in worker
- assert host keys are exactly ['deps','log'] — no token/ims/session/auth fields
- extend skill-script-loader: all fetched URLs under marketplace raw base,
  ao: prefix makes zero fetch calls, path traversal stays under marketplace host
…t page origin

worker imported deps from the page origin (da-live :3000) instead of where
nx2 is served (da-nx :6456 locally, or the nx CDN in prod); resolving against
import.meta.url fixes dep loading regardless of page origin

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
… path + AO flow diagrams

Co-Authored-By: Claude <noreply@anthropic.com>
… inserting a Document

hastToDom returns a full Document node (nodeType 9) when the hast root has
no children — which happens for any empty directive body (e.g. :::info\n:::
produces content '' → parser.parse('') → empty hast root). Inserting a
Document into a Lit template binding throws HierarchyRequestError.

toDOM() now detects a Document result and extracts its body children into a
DocumentFragment before returning. Three regression tests added.

Co-Authored-By: Claude <noreply@anthropic.com>
…ument in the worker

Enforce-by-construction rather than relying on Worker-spec defaults.
Adds three new neuter(self, prop) calls after the existing network/storage
block so the guarantee holds if the bootstrap ever runs outside a dedicated
Worker context (non-Worker isolate, future runtime).

Co-Authored-By: Claude <noreply@anthropic.com>
…lt-in skill (marketplace owns skills)

The docx-to-markdown built-in skill was PoC leftover — under the marketplace
model da-nx must not ship a skill. Moved the convert() body to a test-only
fixture (test/fixtures/skill-scripts/) that exercises the runtime, and removed
nx2/blocks/chat/skills-builtin/ entirely. The real skill lives in the GH
marketplace.

Co-Authored-By: Claude <noreply@anthropic.com>
@anfibiacreativa anfibiacreativa requested a review from mhaack July 1, 2026 08:12
@aem-code-sync

aem-code-sync Bot commented Jul 1, 2026

Copy link
Copy Markdown

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

anfibiacreativa and others added 2 commits July 1, 2026 10:41
- rename `_removed` destructure binding to `_` (no-unused-vars)
- remove dead `fireSkillToolCall` and `stubResolveAndRun` helpers
- change `let updates` to `const` (prefer-const)
- drop unused `runResult` param from `buildPatchedController`
- wrap promise executor bodies in braces (no-promise-executor-return)
- shorten overlong comment line (max-len)
- split `mockFetch` params onto multiple lines (object-curly-newline)
- split ao-prefix fetch stub onto multiple lines (max-statements-per-line)
- name anonymous async functions in e2e tests (func-names)

Co-Authored-By: Claude <noreply@anthropic.com>
// Skills declare deps in execution_dependencies; only names in this map are permitted.
// A skill declaring any name NOT present here is refused before execution.
export const DEPENDENCY_ALLOWLIST = {
fflate: '/nx2/deps/fflate/dist/index.js',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this is something we control - so how will we decide which dependencies we provide?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking that your question comes from the assumption that customers will load their own skills with scripts and how will we know what they need, in order to provide it?
Which is a very valid question in that context. But while we don't have a server-side harness, we would only use this capability for our own skills and hence decide what we need to provide, ourselves.

execution:
entry: convert # exported function name
runtimes: [js] # implementations present (js | py)
capabilities: [] # [] => pure compute => client-eligible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first time capabilities are mentioned - perhaps we should already here either document which are available, or link to the capablities.js file where they are defined.

- the docx skill converts a fixture (`<w:t>hello world</w:t>` → markdown), unescapes
XML entities, and returns `{ error }` on corrupt input without throwing past the runner.

**Explicitly out of scope this round:** wiring into the chat attachment flow (a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is hooking into the chat attachment be the only non-agent trigger that we have planed? Should we already define how this hook will look like in the skill?

@anfibiacreativa anfibiacreativa marked this pull request as draft July 8, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants