Harbor Agent Workflow
Use Harbor Node as a local capability gateway for agents and scripts.
HARBOR_AGENT_WORKFLOW.md
Goal
Use Harbor Node as a local capability gateway for agents and scripts.
Harbor exposes approved actions, not raw connector credentials. Agents should discover ports, inspect action schemas, execute named actions, and respect allow, deny, or require-approval outcomes. Ports stay human-managed. Agents assist by authoring drafts under existing ports, not by creating ports or editing hidden config. Dock can host safe integration metadata later, but hosted import remains operator-driven for now. For authoring new Harbor Ports, actions, and Dock / Hub entries, use docs/HARBOR_PORT_ACTION_AUTHORING.md. For compact execute replies and Business response-pipeline patterns, use docs/HARBOR_RESPONSE_PIPELINES.md.
Default local target:
- Harbor Node base URL:
http://127.0.0.1:11821
Recommended Harbor workflow
- Use GET /v1/status when you need node identity, ports, and counts. - Use GET /health when you only need a fast liveness check.
- Call GET /v1/ports. - Human operators create or maintain ports. - Human operators also initiate OAuth connects and reconnects for OAuth-backed ports such as Gmail. - Agents should not create ports or edit hidden port config.
- Call GET /v1/ports/:portId/actions. - If you are authoring new http_api capabilities, use GET /v1/ports/:portId/action-drafts to inspect drafts separately from live actions.
- Call GET /v1/actions/:actionId. - Read summary, description, approvalMode, and inputSchema before execution.
- Create with POST /v1/ports/:portId/action-drafts. - Validate with POST /v1/action-drafts/:draftId/validate. - Test with POST /v1/action-drafts/:draftId/test. - Request publish with POST /v1/action-drafts/:draftId/request-publish when the caller is not allowed to publish directly. - Publish with POST /v1/action-drafts/:draftId/publish only when Harbor allows that caller mode. - Reject with POST /v1/action-drafts/:draftId/reject. - Drafts stay separate from live actions until published.
- Call POST /v1/actions/:actionId/execute with structured JSON input. - Agent callers can also use the shorter POST /v1/action/:actionRef/:operation route. - Add rm: "c" for a compact response or rm: "o" for an OK-only acknowledgment. - Business nodes may use rp: "<pipeline-id>" to apply a saved response pipeline instead of a raw full reply.
- approved + allow: execution succeeded. - pending_approval + require_approval: capture the returned approvalId, stop execution, and ask a local operator to resolve it through Harbor. - denied + deny: stop and report the policy decision.
- Call GET /v1/approvals or GET /v1/approvals?status=pending. - Inspect GET /v1/approvals/:approvalId. - Approve with POST /v1/approvals/:approvalId/approve or reject with POST /v1/approvals/:approvalId/reject.
- Call GET /v1/audit to confirm what happened or show an operator the recent activity trail.
- Check status.
- Discover ports.
- Discover actions for a port.
- Inspect the action contract.
- Author a draft before making a new capability live.
- Execute the action.
- Handle the outcome correctly.
- Resolve approvals when required.
- Review audit when needed.
Current local MVP surface
- system.echo - system.time - system.rotate-token - system.shutdown
- system.echo -> allow - system.time -> allow - system.rotate-token -> require approval - system.shutdown -> deny - <discord-port-id>.send-message -> allow - <http-api-port-id>.say-hello -> allow - <http-api-port-id>.echo -> allow
- Port:
system-local - Configurable port kind:
webhook - Configurable port kind:
discord_webhook - Configurable port kind:
http_api - Actions:
- Current policy behavior:
Caller mode for agent-safe authoring
Harbor now supports a small local caller-mode distinction:
operatoragentscript
With a bearer token, Harbor now infers agent mode automatically. The minimal agent headers are:
Authorization: Bearer <agent-token>Content-Type: application/json- optional
harbor-name: OpenClaw
Current behavior:
operatormay create or edit portsagentandscriptmay read safe metadata and work with drafts under an existing portagentandscriptmay request publish withPOST /v1/action-drafts/:draftId/request-publish- direct publish remains operator-only unless Harbor is started with
HARBOR_ALLOW_NON_OPERATOR_PUBLISH=true - OpenAPI import remains operator-only for the MVP bulk-authoring surface
- Harbor Pack import/export remains operator-only for the MVP install/share surface
- Dock import remains operator-only for the MVP hosted-catalog surface
- OAuth connect, refresh, and disconnect flows remain operator-only
Compact agent execute example:
{
"i": {
"body": {
"title": "Test Email",
"body": "Hello from Harbor"
}
},
"w": "s",
"rm": "c"
}
Typical compact reply:
{
"ok": true,
"s": "approved",
"x": "exec_123",
"o": {
"draftId": "abc123"
}
}
Saved response pipeline replies use the same compact envelope, but o contains the filtered stored artifact output instead of the full Harbor action output.
PowerShell examples
Set a base URL once:
$baseUrl = "http://127.0.0.1:11821"
Health:
Invoke-RestMethod "$baseUrl/health" | ConvertTo-Json -Depth 8
Status:
Invoke-RestMethod "$baseUrl/v1/status" | ConvertTo-Json -Depth 8
Ports:
Invoke-RestMethod "$baseUrl/v1/ports" | ConvertTo-Json -Depth 8
Create a configured webhook port:
$createBody = @{
kind = "webhook"
label = "Local Mock Webhook"
description = "Configured local verification port"
baseUrl = "$baseUrl/_mock/webhook/tenant-abc/opaque-xyz"
authToken = "super-secret-demo-token"
} | ConvertTo-Json
$createdPort = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports" -ContentType "application/json" -Body $createBody
$createdPort | ConvertTo-Json -Depth 8
$portId = $createdPort.id
Create a configured Discord webhook port:
$discordBody = @{
kind = "discord_webhook"
label = "Discord Alerts"
description = "Send a narrow plain text message to Discord"
webhookUrl = "https://discord.com/api/webhooks/<webhook-id>/<webhook-token>"
} | ConvertTo-Json
$discordPort = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports" -ContentType "application/json" -Body $discordBody
$discordPort | ConvertTo-Json -Depth 8
$discordPortId = $discordPort.id
Create a configured http_api port:
$customHttpBody = @{
kind = "http_api"
label = "Local HTTP API"
description = "Authenticated API behind Harbor"
baseUrl = "http://127.0.0.1:18081"
authToken = "harbor-demo-key"
authHeaderName = "x-api-key"
authTokenPrefix = ""
} | ConvertTo-Json
$httpApiPort = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports" -ContentType "application/json" -Body $customHttpBody
$httpApiPort | ConvertTo-Json -Depth 8
$httpApiPortId = $httpApiPort.id
If Harbor Node is running in Docker on the same machine as the example API container, set baseUrl to http://host.docker.internal:18081 instead.
The returned port metadata is intentionally safe only. Hidden values remain write-only and are never returned after save. When updating a configured webhook port, omit authToken to preserve it, send a new authToken to replace it, or send clearAuthToken: true to remove it. When updating a configured Discord webhook port, omit webhookUrl to preserve it, send a new webhookUrl to replace it, or send clearWebhookUrl: true to remove it. HTTP API ports currently use fixed stored action templates and Harbor-managed hidden auth only; they do not expose generic arbitrary route authoring. Stored HTTP API paths may include bounded placeholders like {owner} and {repo}. Supply those values through input.path; Harbor fills the stored template and keeps path, query, and body inputs separate under the Action Model. HTTP API ports also support safe metadata and hidden auth maintenance through PATCH /v1/ports/:portId; omit authToken to preserve it, send a new authToken to replace it, or send clearAuthToken: true to clear it.
Create an HTTP API action draft:
$draftBody = @{
slug = "draft-hello"
label = "Draft Hello"
description = "Draft GET hello action"
method = "GET"
path = "/v1/hello"
approvalMode = "automatic"
requestBodyMode = "none"
resultMode = "json_summary"
} | ConvertTo-Json
$draft = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports/$httpApiPortId/action-drafts" -ContentType "application/json" -Body $draftBody
$draft | ConvertTo-Json -Depth 8
$draftId = $draft.id
Agent-safe draft creation example:
$headers = @{
"x-harbor-caller-mode" = "agent"
"x-harbor-caller-name" = "OpenClaw"
}
$draft = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports/$httpApiPortId/action-drafts" -Headers $headers -ContentType "application/json" -Body $draftBody
$draft | ConvertTo-Json -Depth 8
List action drafts for an HTTP API port:
Invoke-RestMethod "$baseUrl/v1/ports/$httpApiPortId/action-drafts" | ConvertTo-Json -Depth 8
Validate an action draft:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$draftId/validate" -ContentType "application/json" -Body '{}' | ConvertTo-Json -Depth 8
Test an action draft safely without publishing it:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$draftId/test" -ContentType "application/json" -Body '{"input":{},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Test a repo-scoped templated draft:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$draftId/test" -ContentType "application/json" -Body '{"input":{"path":{"owner":"BreakwaterNinja","repo":"harbor"},"body":{"title":"Example issue","body":"Created through Harbor draft testing."}},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Publish an action draft into a live action:
$published = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$draftId/publish" -ContentType "application/json" -Body '{}'
$published | ConvertTo-Json -Depth 8
$publishedActionId = $published.action.id
Request publish instead of publishing directly:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$draftId/request-publish" -Headers $headers -ContentType "application/json" -Body '{}' | ConvertTo-Json -Depth 8
Inspect the live action after publish:
Invoke-RestMethod "$baseUrl/v1/actions/$publishedActionId" | ConvertTo-Json -Depth 8
Reject a different draft without publishing it:
$rejectDraftBody = @{
slug = "draft-reject"
label = "Draft Reject"
description = "Draft rejection check"
method = "POST"
path = "/v1/echo"
approvalMode = "automatic"
requestBodyMode = "json"
resultMode = "json_summary"
} | ConvertTo-Json
$rejectDraft = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports/$httpApiPortId/action-drafts" -ContentType "application/json" -Body $rejectDraftBody
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/action-drafts/$($rejectDraft.id)/reject" -ContentType "application/json" -Body '{"note":"not publishing this one"}' | ConvertTo-Json -Depth 8
OpenAPI import is intentionally operator-only. Agents should work with the generated drafts after import, not run the import itself.
Operator import example:
$importSpec = @{
openapi = "3.0.3"
info = @{
title = "Custom HTTP API"
version = "1.0.0"
}
servers = @(
@{
url = "http://127.0.0.1:18081"
}
)
paths = @{
"/v1/hello" = @{
get = @{
operationId = "imported-hello"
summary = "Imported Hello"
}
}
"/v1/echo" = @{
post = @{
operationId = "imported-echo"
summary = "Imported Echo"
requestBody = @{
required = $true
content = @{
"application/json" = @{
schema = @{
type = "object"
additionalProperties = $true
}
}
}
}
}
}
}
}
$importBody = @{
document = $importSpec
} | ConvertTo-Json -Depth 20
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports/$httpApiPortId/openapi-import" -ContentType "application/json" -Body $importBody | ConvertTo-Json -Depth 8
Harbor Pack import/export is also operator-only. Packs let an operator export a safe http_api template and later import it into a new degraded port shell plus drafts.
Operator pack export/import example:
$export = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/ports/$httpApiPortId/pack-export" -ContentType "application/json" -Body '{}'
$export | ConvertTo-Json -Depth 12
$packImportBody = @{
pack = $export.pack
portLabel = "Imported Harbor Pack"
portDescription = "Imported from a local Harbor Pack"
} | ConvertTo-Json -Depth 20
$packImport = Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/packs/import" -ContentType "application/json" -Body $packImportBody
$packImport | ConvertTo-Json -Depth 12
$importedPortId = $packImport.port.id
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$importedPortId" -ContentType "application/json" -Body '{"authToken":"harbor-demo-key"}' | ConvertTo-Json -Depth 8
Invoke-RestMethod "$baseUrl/v1/ports/$importedPortId/action-drafts" | ConvertTo-Json -Depth 8
Actions for system-local:
Invoke-RestMethod "$baseUrl/v1/ports/system-local/actions" | ConvertTo-Json -Depth 8
Actions for a configured webhook port:
Invoke-RestMethod "$baseUrl/v1/ports/$portId/actions" | ConvertTo-Json -Depth 8
Describe system.echo:
Invoke-RestMethod "$baseUrl/v1/actions/system.echo" | ConvertTo-Json -Depth 8
Describe a configured action:
Invoke-RestMethod "$baseUrl/v1/actions/$portId.echo-remote" | ConvertTo-Json -Depth 8
Describe the Discord action:
Invoke-RestMethod "$baseUrl/v1/actions/$discordPortId.send-message" | ConvertTo-Json -Depth 8
Describe the HTTP API hello action:
Invoke-RestMethod "$baseUrl/v1/actions/$httpApiPortId.say-hello" | ConvertTo-Json -Depth 8
Update safe metadata without re-entering the hidden auth token:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$portId" -ContentType "application/json" -Body '{"label":"Local Mock Webhook Updated","description":"Updated without re-entering the hidden token"}' | ConvertTo-Json -Depth 8
Replace the hidden auth token explicitly:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$portId" -ContentType "application/json" -Body '{"authToken":"replacement-secret-token"}' | ConvertTo-Json -Depth 8
Clear the hidden auth token explicitly:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$portId" -ContentType "application/json" -Body '{"clearAuthToken":true}' | ConvertTo-Json -Depth 8
Update Discord metadata without re-entering the hidden webhook URL:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$discordPortId" -ContentType "application/json" -Body '{"label":"Discord Alerts Updated","description":"Updated without re-entering the hidden webhook URL"}' | ConvertTo-Json -Depth 8
Replace the hidden Discord webhook URL explicitly:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$discordPortId" -ContentType "application/json" -Body '{"webhookUrl":"https://discord.com/api/webhooks/<webhook-id>/<replacement-token>"}' | ConvertTo-Json -Depth 8
Clear the hidden Discord webhook URL explicitly:
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$discordPortId" -ContentType "application/json" -Body '{"clearWebhookUrl":true}' | ConvertTo-Json -Depth 8
Execute system.echo:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/system.echo/execute" -ContentType "application/json" -Body '{"input":{"message":"hello from powershell"},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Execute the configured webhook echo action:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/$portId.echo-remote/execute" -ContentType "application/json" -Body '{"input":{"message":"hello from configured port","payload":{"source":"powershell"}},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Execute the Discord send-message action:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/$discordPortId.send-message/execute" -ContentType "application/json" -Body '{"input":{"content":"hello from Harbor"},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Execute the HTTP API hello action:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/$httpApiPortId.say-hello/execute" -ContentType "application/json" -Body '{"input":{},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Execute the HTTP API echo action:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/$httpApiPortId.echo/execute" -ContentType "application/json" -Body '{"input":{"message":"hello from Harbor","payload":{"source":"powershell"}},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
If the hidden Discord webhook URL was cleared, Harbor keeps the port record but returns a bounded misconfiguration error until the hidden value is replaced again.
Execute system.rotate-token:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/system.rotate-token/execute" -ContentType "application/json" -Body '{"input":{"reason":"rotate local test token"},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
List pending approvals:
Invoke-RestMethod "$baseUrl/v1/approvals?status=pending" | ConvertTo-Json -Depth 8
Inspect a specific approval:
Invoke-RestMethod "$baseUrl/v1/approvals/$approvalId" | ConvertTo-Json -Depth 8
Approve a pending approval:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/approvals/$approvalId/approve" -ContentType "application/json" -Body '{"note":"approved locally","resolvedBy":{"kind":"user","name":"powershell-operator"}}' | ConvertTo-Json -Depth 8
Reject a pending approval:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/approvals/$approvalId/reject" -ContentType "application/json" -Body '{"note":"rejected locally","resolvedBy":{"kind":"user","name":"powershell-operator"}}' | ConvertTo-Json -Depth 8
Execute system.shutdown:
Invoke-RestMethod -Method Post -Uri "$baseUrl/v1/actions/system.shutdown/execute" -ContentType "application/json" -Body '{"input":{"confirm":true},"requestedBy":{"kind":"script","name":"powershell-example"}}' | ConvertTo-Json -Depth 8
Audit:
Invoke-RestMethod "$baseUrl/v1/audit?limit=20" | ConvertTo-Json -Depth 8
Outbound failure handling
For real configured outbound actions, Harbor keeps error behavior small and predictable:
- requests time out using the local
HARBOR_OUTBOUND_TIMEOUT_MSsetting - failures are classified as
timeout,network_error,remote_4xx,remote_5xx,invalid_remote_response, ormisconfigured_port - error payloads and audit summaries stay bounded and do not return hidden URLs, tokens, or raw secret-bearing paths
curl examples
Use curl.exe on Windows so PowerShell does not rewrite the command into Invoke-WebRequest.
Health:
curl.exe http://127.0.0.1:11821/health
Status:
curl.exe http://127.0.0.1:11821/v1/status
Ports:
curl.exe http://127.0.0.1:11821/v1/ports
Actions for system-local:
curl.exe http://127.0.0.1:11821/v1/ports/system-local/actions
Describe system.echo:
curl.exe http://127.0.0.1:11821/v1/actions/system.echo
Execute system.echo:
curl.exe -X POST http://127.0.0.1:11821/v1/actions/system.echo/execute -H "content-type: application/json" -d "{\"input\":{\"message\":\"hello from curl\"},\"requestedBy\":{\"kind\":\"script\",\"name\":\"curl-example\"}}"
Execute system.rotate-token:
curl.exe -X POST http://127.0.0.1:11821/v1/actions/system.rotate-token/execute -H "content-type: application/json" -d "{\"input\":{\"reason\":\"rotate local test token\"},\"requestedBy\":{\"kind\":\"script\",\"name\":\"curl-example\"}}"
Execute system.shutdown:
curl.exe -X POST http://127.0.0.1:11821/v1/actions/system.shutdown/execute -H "content-type: application/json" -d "{\"input\":{\"confirm\":true},\"requestedBy\":{\"kind\":\"script\",\"name\":\"curl-example\"}}"
Audit:
curl.exe "http://127.0.0.1:11821/v1/audit?limit=20"
Python example commands
Install the small dependency once:
python -m pip install -r .\examples\python\requirements.txt
Then run:
python .\examples\python\harbor_client.py health
python .\examples\python\harbor_client.py status
python .\examples\python\harbor_client.py ports
python .\examples\python\harbor_client.py actions system-local
python .\examples\python\harbor_client.py action system.echo
python .\examples\python\harbor_client.py execute system.echo --input-file .\examples\python\payloads\system.echo.json
python .\examples\python\harbor_client.py execute system.rotate-token --input-file .\examples\python\payloads\system.rotate-token.json
python .\examples\python\harbor_client.py execute system.shutdown --input-file .\examples\python\payloads\system.shutdown.json
python .\examples\python\harbor_client.py audit --limit 20
The Python helper defaults to HARBOR_NODE_BASE_URL=http://127.0.0.1:11821 and lets you override that with --base-url or the environment variable. Use --input-file on Windows when you want copy-paste-safe execution examples. /_mock/webhook/* is a local verification surface only and should not be treated as Harbor's product connector model.