Skip to content

Open Harness Server subcommands - #1853

Open
SSharma-10 wants to merge 175 commits into
mainfrom
feat/agents-subcommands
Open

Open Harness Server subcommands#1853
SSharma-10 wants to merge 175 commits into
mainfrom
feat/agents-subcommands

Conversation

@SSharma-10

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread commands/agents.go Outdated
Comment thread commands/agents.go Outdated

@logwolvy logwolvy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Assuming, relevant godo changes for sending agent spec yaml are done.
Please address the comments and we should be good to merge.

Comment thread commands/agents.go Outdated
Comment thread commands/agents.go Outdated
Comment thread commands/agents.go Outdated
SSharma-10 and others added 6 commits June 3, 2026 16:52
doctl agent attach connected but printed nothing: the harness-api SSE
stream emits the SPI canonical event envelope (type/data/timestamp/
tenant_id, dot-separated event names) while vendored godo decoded
kind/payload/at/team_id, so every event fell through renderEvent.

- godo HostedAgentEvent: decode the SPI wire via UnmarshalJSON and switch
  the HostedAgentEventKind constants to the dot-separated SPI names.
- godo ErrorResponse: also read the nested {"error":{code,message}}
  envelope so failed calls show the server's reason, not a bare status.
- agents: align event payload structs/rendering and HITL tracking to the
  SPI data shapes, and acknowledge each submit with its run id so the
  agent's startup latency isn't mistaken for a hang (which caused
  duplicate submits).
- tests for the wire decode, rendering, nested-error surfacing, and ack.

The vendored godo edits mirror the fix being upstreamed to
digitalocean/godo and are in-tree until godo is re-vendored.

Co-authored-by: Cursor <cursoragent@cursor.com>
logwolvy
logwolvy previously approved these changes Jun 8, 2026

@logwolvy logwolvy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I will be raising a PR for stream related fixes on doctl and godo.

Comment thread internal/deviceid/deviceid_darwin.go
Comment thread internal/deviceid/deviceid_windows.go
Comment thread commands/agents_test.go
@@ -0,0 +1,1215 @@
/*
Copyright 2026 The Doctl Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apple Pay

Vendors glamour and its transitive deps (chroma, goldmark, bluemonday,
etc.) and bumps lipgloss/termenv so `doctl agents` can render the
agent's markdown replies with syntax-highlighted code blocks. Also
re-syncs the pinned godo vendor tree (adds Session.SandboxID).

Co-authored-by: Cursor <cursoragent@cursor.com>
SSharma-10 and others added 30 commits August 25, 2026 22:19
agents: accept hermes as a known adapter
…1931)

Starting an agent and then having to run a second command to talk to it
inverted the common case: the reason to start a session is to use it.
`start` now opens the chat once the session is ready, and `--detach` (-d)
keeps the old behaviour of stopping at the ready summary.

Two conditions imply detach so this stays safe for non-humans: -o json,
and a stdin/stdout that is not a terminal. Without the second, every
script and CI job already calling `agents start` would have gone from
printing a summary and exiting to blocking forever in a TUI that nothing
can drive.

`--no-attach` keeps working as a hidden alias of --detach, so `run` callers
are unaffected. `run` is now identical to `start` in behaviour; it shares
the same decision helper and Codex-proxy tip rather than a second copy.
Attaching to a session that is still booting mishandled input in three
ways. Follow-up to MARSOHS-972.

The caret never moved while typing. The warm-up repaint computed the
right caret column and then discarded it by ending with DECRC, which
restored the cursor saved at the top of the same paint. Warm-up
suppresses echo and repaints instead, so that paint was the only thing
that could advance the caret. The banner now renders top-down from a
known anchor and stops where the caret belongs.

Submitted messages vanished. On Enter the input buffer is cleared
first, then the commit to scrollback was skipped whenever the banner
was visible, leaving no record of what had been queued. Submitted lines
now land above the pinned banner.

Only the first message was ever sent. The second was dropped outright:
not sent, not shown, not returned to the input line. The guard was
unnecessary, since a send only succeeds once the guest run accepts
input, and from there OHR queues turns in order. Every message is now
sent and counted, the banner reports the running total, and a failed
send restores the text so nothing typed is lost.

Also fixes stale spinner rows: plain writes ignored the banner's row
accounting, landing on the prompt row and pushing the prompt down, so
each later frame repainted the spinner one row lower. Scrollback writes
now go above the banner, and clearing it deletes its rows rather than
blanking them in place.
* agents: add --allow-grace to triggers rotate-secret

rotate-secret now revokes the old webhook secret immediately by default
(server-side MARSOHS-1078). Add --allow-grace to opt into the bounded
grace window for routine key-hygiene rotations, where in-flight deliveries
signed with the old secret need to keep verifying briefly. Never use it
after a secret is compromised.

When --allow-grace is set the response carries previous_secret_expires_at,
which is surfaced in both text and json output so operators know when the
old secret dies.

Co-authored-by: Cursor <cursoragent@cursor.com>

* agents: replace --allow-grace with --revoke-previous on rotate-secret

Reverses the default this branch previously shipped. rotate-secret keeps its
grace window: the provider side holds only one secret -- a GitHub webhook has a
single Secret field -- so it keeps signing with the old value until a human
pastes the new one in, and revoking on the call would turn every routine
rotation into a delivery outage lasting as long as that handoff takes.

--revoke-previous is the breach path instead. It retires the old secret at once
and accepts that gap. Either way the command now states the outcome -- the
expiry instant, or that the old secret is already dead -- because "rotated"
alone leaves an operator unable to tell whether a leaked secret still works.

Tracks the server-side contract in cthulhu (MARSOHS-1078), which is additive:
?revoke_previous=true, and previous_secret_revoked in the response.

Co-authored-by: Cursor <cursoragent@cursor.com>

* agents: match the upstream godo rotate-secret options struct

Mirrors digitalocean/godo#1092, which takes an options pointer rather than a
bare bool (consistent with the other List* calls in that file) and returns the
expiry as *Timestamp. Vendor stays a patch only until that lands and we can
bump the dependency.

Co-authored-by: Cursor <cursoragent@cursor.com>

* agents: read the rotate-secret outcome instead of inferring it

The CLI treated a missing previous_secret_expires_at as proof the old secret
was revoked. The API sets exactly one of the two fields, so the revoked flag
can just be read; inferring it meant any other cause of an absent expiry would
tell an operator a live secret was dead. When neither field arrives the CLI now
says so rather than guessing.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: SSharma-10 <shivanisharma@digitalocean.com>
…1932)

Omit for the 5m default, --grace-period 0 to revoke immediately, or a
positive seconds value for a custom handoff window (server max 1h).

Co-authored-by: Cursor <cursoragent@cursor.com>
….yaml (#1930)

Naming a manifest required a flag for the most common invocation, so the
thing everyone types first -- `doctl agents start agents.yaml` -- was
silently ignored and fell through to "one of --harness, --spec, or
--config-id is required".

start, run and validate now resolve the manifest most-explicit-first:
--spec wins over a positional path, which wins over an agents.yaml
discovered in the working directory. --spec stays canonical because it is
doctl's word for a declarative file (apps, dedicated-inference); -f and
--file are added as aliases for the kubectl/compose/planoai muscle memory.

Implicit sources are never silent: a discovered file is announced, and
suppressed under -o json so machine output stays parseable. Discovery only
runs when no other source was selected, so --harness in a directory holding
an agents.yaml stays unambiguous, and pairing a positional path with
--harness or --config-id is a clear error rather than a silent winner.

Co-authored-by: SSharma-10 <shivanisharma@digitalocean.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Surface harness-api create-time policy advisories after session create
for manifest, config, and config-start paths; vendor godo Warnings field.

Co-authored-by: Cursor <cursoragent@cursor.com>
…HS-1012) (#1938)

translateEvent's TokenChunk case decoded only `text`, so a chunk marked
is_reasoning was forwarded as item/agentMessage/delta exactly like the
answer. The client concatenates that channel into the visible reply, so the
model's thinking trace was displayed as the response.

Reasoning cannot be re-routed here: item/agentMessage/delta is the only text
channel the facade speaks, and the protocol has no reasoning item. Codex
sessions never hit this path for reasoning -- raw.go forwards their native
frames, which carry it as its own item -- so the fallback is what non-codex
runtimes take, and dropping is the only faithful option until the protocol
gains somewhere to put it.

The skip sits ahead of the item/started block deliberately: a turn that leads
with reasoning would otherwise announce an agentMessage item whose only
content we then refuse to send.

commands/agents.go already handles this properly via reasoningStreamer, which
renders the trace in a separate muted channel. That is the better treatment
and is left alone; only the facade, which has no such channel, is changed.

Confirmed against production: a cursor session's live SSE carries
is_reasoning on every token delta -- 4 true (source_event_type
session/update:agent_thought_chunk) and 21 false
(session/update:agent_message_chunk) on a single run.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts by keeping hosted-agents godo (v1.205.0-beta.3) and
absorbing main's Gradient AI simulations surface (args + vendored APIs).

Co-authored-by: Cursor <cursoragent@cursor.com>
Three verbs had converged into near-synonyms: `start` and `run` both created
a session and attached by default, leaving `--no-attach`/`--detach` as the only
thing distinguishing them, and `attach` was a third spelling of what `run`
already ended in. Two verbs now split on whether you end up looking at the
session:

  create  makes a session and leaves it alone. Never attaches.
  launch  puts you in one -- creating it first when given creation flags or a
          manifest, attaching to an existing session when given its name or ID,
          resuming it first if it is paused.

`attach`, `up` and `chat` are aliases of `launch`, so existing invocations keep
working. `--no-attach` and `--detach` are gone, since `create` is now what "do
not attach" means, and `-o json` moves to `create` alone because `launch` is
inherently interactive. A positional argument on `launch` is read as a manifest
when it names a readable file and as a session reference otherwise; creation
flags settle it before the filesystem is consulted, and an argument that is
neither reports both readings instead of guessing.

Also adds four flags the split made room for:

--from-config replaces --config-id and accepts a config name as well as an ID,
mirroring the courtesy resolveSessionRef already extends to sessions. Wired
into create, launch, triggers, and the positional argument on config
get/delete/list-sessions/start-session.

--secret NAME=VALUE (also NAME=@path, NAME=-) fills a secret slot the manifest
declares, so a checked-in agents.yaml can name the credentials it needs without
carrying them. There is no secret API to call -- a tenant secret only exists as
a write-only value on a manifest slot -- so this is manifest surgery. The
key=value/@file/- grammar moves out of `doctl secrets` into a shared helper so
both commands parse it identically.

--dry-run prints the fully resolved manifest with secrets redacted and no API
calls, which is what lets a working --harness invocation be promoted into a
config or a trigger without hand-writing YAML.

--on-hitl approve|reject|defer keeps create attached headlessly past ready,
resolving every approval with a fixed policy until the run finishes. Approvals
were the last thing in an unattended run that still required a human.

Two failures found while exercising the pipe end to end: --dry-run erred on a
${VAR} that was not set locally, which demanded a credential merely to print a
manifest whose purpose is to carry it elsewhere (it now stays a placeholder and
is reported on stderr, keeping stdout clean YAML); and a redacted manifest piped
onward without --secret stored the literal string "REDACTED" as the credential,
succeeding silently and failing much later inside a sandbox. Redacted
placeholders are now rejected on every path that sends a manifest.

Triggers take --from-config as a client-side copy, since
HostedAgentTriggerCreateRequest has no config field: the manifest is fetched and
sent as SessionTemplate. It is a copy rather than a link, and a config's stored
secret values never return to a client, so --secret must be supplied again at
trigger-create time. Both caveats are documented; a backend-side config
reference would remove them.
…1942)

M0: captured a real `opencode serve` + `opencode attach` pair (1.18.25)
through a logging relay. Findings encoded here: the TUI subscribes to
/global/event (wrapped {payload:{id,type,properties}} envelope, not the
bare instance-stream shape), and the attach burst is ~30 read-only
bootstrap routes including a newer /api/* surface.

M1: --type opencode behind start-proxy. New ServeHTTP/ServeHTTPListener
transport (REST+SSE facades have no JSON-RPC framing to pump; BaseContext
roots request contexts in the server ctx so a blocked SSE handler is
released on shutdown). The opencode facade answers the captured burst,
streams server.connected first, enforces one event-stream consumer,
refuses /share, and 404s+logs unknown routes as the drift detector. A
synthetic single provider/model catalog gives the TUI a selected model;
real bridging lands in M2.

Verified live: opencode 1.18.25 attaches and renders an empty ready
session with zero unhandled routes.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
port-forward previously opened the local tunnel without waking a paused
session, unlike launch/attach. Resume first and wait until ready so the
tunnel works without a manual resume step.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.