Creating components (inline/docker). Dynamic ports, retry policies, PTY patterns, IsolatedContainerVolume.
Full guide: docs/development/component-development.mdx
worker/src/components/<category>/<component-name>.ts
Categories: security/, core/, ai/, notification/, manual-action/
<namespace>.<tool>.<action>
Examples: shipsec.dnsx.run, core.http.request, ai.llm.generate
import { z } from 'zod';
import { defineComponent, inputs, outputs, port } from '@shipsec/component-sdk';
export default defineComponent({
id: 'category.tool.action',
label: 'My Component',
category: 'security', // or: core, ai, notification, manual_action
runner: { kind: 'inline' }, // or: docker
inputs: inputs({
target: port(z.string(), { label: 'Target Host' }),
}),
outputs: outputs({
success: port(z.boolean(), { label: 'Success' }),
}),
async execute({ inputs }, context) {
// ... logic ...
return { success: true };
}
});
Check existing components in same category for patterns
ls worker/src/components/<category>/
Copy structure from similar component — don't start from scratch
Always use defineComponent helper with separated schemas:
inputs() + port()outputs() + port()parameters() + param() (optional)__tests__/<component>.test.tsFor Docker components:
entrypoint: 'sh', command: ['-c', 'tool "$@"', '--']IsolatedContainerVolume for file I/Oworker/src/components/security/dnsx.ts□ ID follows pattern: namespace.tool.action
□ File in correct category folder
□ inputs/outputs/parameters defined with port()/param() helpers
□ execute() receives { inputs, params }
□ Docker: shell wrapper pattern used
□ Docker with files: IsolatedContainerVolume used
□ Unit test created
□ Exported as default (componentRegistry.register is handled by defineComponent)
runner: { kind: 'inline' }
// Just write TypeScript in execute()
runner: {
kind: 'docker',
image: 'tool:latest',
entrypoint: 'sh',
command: ['-c', 'tool "$@"', '--'],
network: 'bridge',
}
// ⚠️ Shell wrapper required for PTY
→ See: docs/development/component-development.mdx#docker-component-requirements
import { IsolatedContainerVolume } from '../../utils/isolated-volume';
const volume = new IsolatedContainerVolume(tenantId, context.runId);
try {
await volume.initialize({ 'input.txt': data });
// volumes: [volume.getVolumeConfig('/path', true)]
// Note: Permissions are auto-set for nonroot containers
} finally {
await volume.cleanup();
}
→ See: docs/development/isolated-volumes.mdx
// Supported types: text, number, file, json, array, secret
const runtimeInputs = [
{ id: 'apiKey', label: 'API Key', type: 'secret', required: true },
{ id: 'targets', label: 'Targets', type: 'array', required: true },
];
// Secret type renders as password field in UI
→ See: docs/development/component-development.mdx#entry-point-runtime-input-types
| Type | Function | UI Location | Use Case |
|------|----------|-------------|----------|
| Inputs | inputs() + port() | Canvas handles | Runtime data (target, apiKey, fileId) |
| Parameters| parameters() + param() | Sidebar form | Static config (model, timeout, enum) |
Note on Inputs: You can set valuePriority: 'manual-first' in port metadata to prioritize manual overrides over connected data.
parameters: parameters({
model: param(z.string().default('gpt-4'), {
label: 'Model Name',
editor: 'select',
options: [{ label: 'GPT-4', value: 'gpt-4' }],
}),
timeout: param(z.number().min(1), {
label: 'Timeout',
editor: 'number'
}),
})
Editors: text, textarea, number, boolean, select, multi-select, json, secret.
async execute({ inputs, params }, context) {
context.logger.info('...'); // Logs to UI timeline
context.emitProgress('...'); // Progress events
await context.secrets?.get('KEY'); // Encrypted secrets
await context.storage?.downloadFile(inputs.fileId); // MinIO files
await context.artifacts?.upload({...}); // Save artifacts
}
import { ValidationError, AuthenticationError, ServiceError } from '@shipsec/component-sdk';
// Non-retryable (immediate fail)
throw new ValidationError('Bad input', { fieldErrors: {...} });
throw new AuthenticationError('Invalid API key');
// Retryable (Temporal will retry)
throw new ServiceError('API down', { statusCode: 503 });
→ See: docs/development/component-development.mdx#error-handling
# Unit tests (mocked, fast)
bun --cwd worker test
# Integration tests (real Docker)
ENABLE_DOCKER_TESTS=true bun --cwd worker test
# E2E tests (full stack - requires `just dev`)
RUN_E2E=true bun --cwd e2e-tests test
| Mistake | Fix |
|---------|-----|
| Docker without shell wrapper | Use entrypoint: 'sh', command: ['-c', 'tool "$@"', '--'] |
| Direct file mounts in Docker | Use IsolatedContainerVolume |
| Missing finally for volume cleanup | Always await volume.cleanup() in finally |
| Missing port() wrapper | All input/output schemas must use port() |
| Mixing inputs and parameters | Use inputs() for runtime ports and parameters() for design-time config |
| Throwing plain Error | Use SDK errors: ValidationError, ServiceError, etc. |
| What | Where |
|------|-------|
| Full docs | docs/development/component-development.mdx |
| Isolated volumes | docs/development/isolated-volumes.mdx |
| SDK source | packages/component-sdk/src/ |
| Good example (Docker) | worker/src/components/security/dnsx.ts |
| Good example (inline) | worker/src/components/core/http-request.ts |
| E2E tests | e2e-tests/ |
npx skills add ShipSecAI/component-development下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer