Documentation

Source-of-truth docs, references, plans, and product material across Harbor surfaces.

Flows And Authoring

Harbor Agent Workflow

Use Harbor Node as a local capability gateway for agents and scripts.

agentsworkflow
Source: HARBOR_AGENT_WORKFLOW.md

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

- 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.

  1. Check status.
  2. Discover ports.
  3. Discover actions for a port.
  4. Inspect the action contract.
  5. Author a draft before making a new capability live.
  6. Execute the action.
  7. Handle the outcome correctly.
  8. Resolve approvals when required.
  9. 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:

  • operator
  • agent
  • script

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:

  • operator may create or edit ports
  • agent and script may read safe metadata and work with drafts under an existing port
  • agent and script may request publish with POST /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:

json
{
  "i": {
    "body": {
      "title": "Test Email",
      "body": "Hello from Harbor"
    }
  },
  "w": "s",
  "rm": "c"
}

Typical compact reply:

json
{
  "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:

powershell
$baseUrl = "http://127.0.0.1:11821"

Health:

powershell
Invoke-RestMethod "$baseUrl/health" | ConvertTo-Json -Depth 8

Status:

powershell
Invoke-RestMethod "$baseUrl/v1/status" | ConvertTo-Json -Depth 8

Ports:

powershell
Invoke-RestMethod "$baseUrl/v1/ports" | ConvertTo-Json -Depth 8

Create a configured webhook port:

powershell
$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:

powershell
$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:

powershell
$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:

powershell
$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:

powershell
$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:

powershell
Invoke-RestMethod "$baseUrl/v1/ports/$httpApiPortId/action-drafts" | ConvertTo-Json -Depth 8

Validate an action draft:

powershell
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:

powershell
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:

powershell
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:

powershell
$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:

powershell
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:

powershell
Invoke-RestMethod "$baseUrl/v1/actions/$publishedActionId" | ConvertTo-Json -Depth 8

Reject a different draft without publishing it:

powershell
$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:

powershell
$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:

powershell
$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:

powershell
Invoke-RestMethod "$baseUrl/v1/ports/system-local/actions" | ConvertTo-Json -Depth 8

Actions for a configured webhook port:

powershell
Invoke-RestMethod "$baseUrl/v1/ports/$portId/actions" | ConvertTo-Json -Depth 8

Describe system.echo:

powershell
Invoke-RestMethod "$baseUrl/v1/actions/system.echo" | ConvertTo-Json -Depth 8

Describe a configured action:

powershell
Invoke-RestMethod "$baseUrl/v1/actions/$portId.echo-remote" | ConvertTo-Json -Depth 8

Describe the Discord action:

powershell
Invoke-RestMethod "$baseUrl/v1/actions/$discordPortId.send-message" | ConvertTo-Json -Depth 8

Describe the HTTP API hello action:

powershell
Invoke-RestMethod "$baseUrl/v1/actions/$httpApiPortId.say-hello" | ConvertTo-Json -Depth 8

Update safe metadata without re-entering the hidden auth token:

powershell
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:

powershell
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:

powershell
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:

powershell
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:

powershell
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:

powershell
Invoke-RestMethod -Method Patch -Uri "$baseUrl/v1/ports/$discordPortId" -ContentType "application/json" -Body '{"clearWebhookUrl":true}' | ConvertTo-Json -Depth 8

Execute system.echo:

powershell
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:

powershell
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:

powershell
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:

powershell
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:

powershell
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:

powershell
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:

powershell
Invoke-RestMethod "$baseUrl/v1/approvals?status=pending" | ConvertTo-Json -Depth 8

Inspect a specific approval:

powershell
Invoke-RestMethod "$baseUrl/v1/approvals/$approvalId" | ConvertTo-Json -Depth 8

Approve a pending approval:

powershell
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:

powershell
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:

powershell
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:

powershell
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_MS setting
  • failures are classified as timeout, network_error, remote_4xx, remote_5xx, invalid_remote_response, or misconfigured_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:

powershell
curl.exe http://127.0.0.1:11821/health

Status:

powershell
curl.exe http://127.0.0.1:11821/v1/status

Ports:

powershell
curl.exe http://127.0.0.1:11821/v1/ports

Actions for system-local:

powershell
curl.exe http://127.0.0.1:11821/v1/ports/system-local/actions

Describe system.echo:

powershell
curl.exe http://127.0.0.1:11821/v1/actions/system.echo

Execute system.echo:

powershell
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:

powershell
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:

powershell
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:

powershell
curl.exe "http://127.0.0.1:11821/v1/audit?limit=20"

Python example commands

Install the small dependency once:

powershell
python -m pip install -r .\examples\python\requirements.txt

Then run:

powershell
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.