feat: add skill scripts runtime#572
Conversation
- 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>
|
Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
Commits
|
- 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', |
There was a problem hiding this comment.
IIUC this is something we control - so how will we decide which dependencies we provide?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
THIS CODE IS NOT MEANT TO STAY PERMANENTLY. IT'S AN ITERIM SOLUTION FOR CODE EXECUTION.
Summary
host.depsinjection so scripts can call DA APIs without bundling credentials.attachmentRefto 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 (targetingadobe/skills) for prod. See the skill-script-runtime migration plan.What Changed
skill_run_script→ da-nx worker → result back to agenthost.depsinjection: injects authenticated DA API helpers into skill script scopeattachmentRef→ bytes: binary payload conversion for attachment-capable skills.docxaccept: file picker now accepts.docxuploadsTest Plan
npm testpasses (all relevant test suites)host.depsinjection: skill script can call a DA API helper without holding its own credentialsattachmentRef→ bytes: upload a file attachment; skill receives binary payload correctly.docxaccept: file picker shows.docxas an accepted typeRisks / Follow-ups
exp-workspace/skills(ew/) is hardcoded — must move to config sheet targetingadobe/skillsbefore prodhost.depsReference
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