Skip to content

service add / deploy: --endpoint udp|tcp+udp, and show a udp service's udp:// address - #375

Merged
jwfing merged 3 commits into
mainfrom
jwfing/udp-endpoint
Oct 16, 2026
Merged

jwfing merged 3 commits into
mainfrom
jwfing/udp-endpoint

Conversation

@jwfing

@jwfing jwfing commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

The CLI half of InsForge/instacloud-platform#681, which widens compute endpoint from http | tcp to http | tcp | udp | tcp+udp. The compute plane has served public UDP on its v6 lane in every region since 2026-10-14. This PR follows the shape of #368 (--endpoint tcp).

insta service add compute dns --image my/dns --port 5000 --endpoint udp
# added compute service dns … — udp/5000 (address after the first deploy)
insta deploy --image my/dns:2 --port 5000 --endpoint tcp+udp
# deployed my/dns:2 -> tcp://<host>:5000, udp://<host>:5000 (branch main, group dns)

Changes

  • service add / deploy: --endpoint <http|tcp|udp|tcp+udp>.
  • Help text (where service add / deploy: --endpoint http|tcp, and show a tcp service's tcp:// address #368 put the tcp notes): IPv6 clients only, no platform TLS, no custom domain. Refused ports are described by kind: amplification-prone (DNS, NTP, SNMP, memcached, …) and platform-reserved ports; tcp+udp refuses both lists. A datagram wakes a suspended service, and what the wake held is re-sent until the service first replies, so an app may see a datagram twice. A receive-only udp service needs --always-on.
  • Archive (source-dir) lane: the one gated body carries udp / tcp+udp. service add / deploy: --endpoint http|tcp, and show a tcp service's tcp:// address #368's check that accepts an empty url when endpointHost is present already covers them. The live read's endpoint is carried through (see below).
  • Output:
    • service list and the service add line render udp://host:port, and for tcp+udp tcp://host:port, udp://host:port. Before a deploy has confirmed the host they render udp/<port> or tcp+udp/<port> (address after the first deploy).
    • A v6 row never shows an HTTP domain.
    • tcp and http rows are byte-for-byte unchanged, and tests pin the exact lines.
    • deploy takes the scheme from the result's endpoint. #681 returns it beside endpointHost on POST /deploy and on a live archive-deploy read, including a redeploy that passed no endpoint, where it reports the endpoint the service kept. --endpoint is used only if the result has a host but no endpoint (an older platform). If neither is available, deploy prints host:port (endpoint unchanged; \insta service list` shows its protocol)rather than guessingtcp://. An endpoint value this CLI does not know is treated as absent. **A plain redeploy of a tcp service prints exactly tcp://host:port`, as before.**
    • --json: the image lane passes the result through, now including endpoint. The archive lane adds endpoint beside endpointHost. service list --json passes endpoint / endpoint_host through.

Decisions

  1. The result's endpoint decides the scheme, and the flag is only a fallback. The result reports the endpoint the service actually ended up on. Without it, a plain redeploy of a udp service would print tcp://. The scheme-less line appears only for an older platform that returns a host with no endpoint and no flag. One service add / deploy: --endpoint http|tcp, and show a tcp service's tcp:// address #368 test row (reports a tcp deploy) now passes 'tcp' explicitly, because it models a result that carries no endpoint.
  2. The tcp+udp rendering is tcp://h:p, udp://h:p: one scheme per protocol, so each part is a valid URI on its own. tcp+udp:// is not a real scheme.
  3. Only tcp+udp is accepted (case and whitespace normalized like the other values). udp+tcp is refused because the platform spells the pair one way only.
  4. service add / deploy: --endpoint http|tcp, and show a tcp service's tcp:// address #368's existing tests changed in three places: the refusal message, udp moving from the refused list to the accepted one, and the tcp deploy-target row above. These follow from the contract change; nothing was loosened.

Promise → test (test/endpoint-udp.test.ts, one row per outcome)

promise test
udp, tcp+udp, UDP, TCP+UDP parse parseEndpoint: udp and tcp+udp › %j is %s
udp+tcp, tcp+, tcp,udp, tcp udp, quic refused, with the message … › %j is refused
service add sends udp / tcp+udp sends endpoint %s
no CLI port mirror (53 + udp is sent) does not mirror the platform port list
udp on redis, tcp+udp on storage, and a reversed pair all fail locally %s fails locally
added line: udp, tcp+udp, and not yet deployed three the added line … tests
list: udp, tcp+udp, both not yet deployed, a stray domain ignored, tcp and http unchanged (exact lines) service list: udp and tcp+udp rows table
v6Address for tcp is exactly tcpAddress v6Address for tcp is tcpAddress…
service list --json passes endpoint fields through --json passes endpoint and endpoint_host through untouched
deploy body: udp / tcp+udp rides the deploy body as %s
result endpoint tcp / udp / tcp+udp with no flag (tcp is the exact tcp:// line) deployedTarget reads the endpoint from the result › result … no flag
when the flag and the result disagree, the result wins (two rows) … the result wins
host without endpoint falls back to the flag, or to no scheme; an unknown result value counts as absent host without endpoint … / … does not know …
older platform: the flag gives udp or tcp+udp; no --port prints :? reports %s table
http: the URL is unchanged, with or without --endpoint http http: the URL, unchanged rows
the archive body carries udp / tcp+udp and accepts an empty url with a host sends endpoint %s and accepts an empty URL…
the archive read carries endpoint, an older platform omits it, an unknown value is dropped carries the read's endpoint / an older platform omits it / an unknown value is dropped
deploy() dies on udp+tcp before ApiClient.load / requireProject an invalid pair (udp+tcp) dies before…
deploy() archive lane: normalized udp in the body, udp:// line, --json endpointHost two archive lane … tests
deploy() image lane: tcp+udp in the body, both-scheme line, --json pass-through two image lane … tests
deploy() image lane, plain tcp redeploy: the exact deployed img:2 -> tcp://… line, with no endpoint in the body image lane, plain redeploy of a tcp service …
deploy() archive lane, plain udp redeploy: udp:// from the read, and --json carries endpoint archive lane, plain redeploy of a udp service …

Gates

  • npm run typecheck: clean.
  • npm test: 105 files, 2225 tests passed.

Negative controls

I broke each piece alone and ran the full suite. Each run failed where expected, then the file was restored with git checkout, and git diff --quiet confirmed the restore was exact.

sabotage result
parseEndpoint back to http/tcp only 15 failed
v6Address renders tcp+udp as udp only 2 failed (tcp+udp added line, list row)
list line treats only tcp as v6 5 failed (every udp/tcp+udp list row)
added line treats only tcp as v6 3 failed
deployedTarget always prints tcp:// 6 failed
deployedTarget guesses tcp:// without --endpoint 1 failed (the scheme-less row)
deploy() drops the endpoint before deployedTarget 2 failed (archive and image lane lines)
archive body filter back to http/tcp 3 failed
servicesAddRequestBody drops udp / tcp+udp 3 failed
deployRequestBody drops udp / tcp+udp 3 failed
deployedTarget ignores the result's endpoint 7 failed (all result rows, plus both plain-redeploy deploy() tests)
the flag wins over the result 2 failed (both "result wins" rows)
deployedTarget trusts an unknown result endpoint 1 failed
the archive read drops endpoint 2 failed
the archive read keeps an unknown endpoint 1 failed
the archive --json drops endpoint 1 failed

Order and companions

  • Depends on instacloud-platform#681 being deployed, including its endpoint field on the deploy result and the archive read. Before #681, the platform's schema should refuse endpoint: udp; I took that from #681's enum and did not test it. Against a platform without the result endpoint field, the CLI falls back to the flag or to the scheme-less line, so it never prints a wrong scheme.
  • Command reference: cli-reference.md in InsForge/instacloud-skills (AGENTS rule 4) will follow in its own PR, as service add / deploy: --endpoint http|tcp, and show a tcp service's tcp:// address #368's did.
  • MCP: this adds new values to an existing flag on existing leaves, not a new leaf, so rule 6 does not apply. The MCP endpoint argument should widen in InsForge/instacloud-mcp.

🤖 Generated with Claude Code

…e's udp:// address

The compute plane serves public UDP on its v6 lane in every region (live
2026-10-14) and instacloud-platform#681 widens `endpoint` to
http | tcp | udp | tcp+udp. This is the CLI half, mirroring #368 (tcp):

- `--endpoint` on `service add` and `deploy` accepts udp and tcp+udp,
  parsed locally (a typo or `udp+tcp` fails before any network call) and
  sent only when given. Port rules stay the platform's answer, as for tcp.
- The archive lane's body carries the new values.
- `service list` / the added line render udp as udp://host:port and
  tcp+udp as `tcp://host:port, udp://host:port` (or `<proto>/<port>
  (address after the first deploy)` before a deploy confirms the host).
  tcp and http rows are byte-for-byte unchanged.
- `deploy` prints the scheme from --endpoint; the deploy result names the
  host but not the protocol, so a v6 redeploy without --endpoint prints
  `host:port (endpoint unchanged; ...)` instead of guessing tcp://.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot 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.

3 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/index.ts">

<violation number="1" location="src/index.ts:553">
P2: `deploy --help` enables UDP without warning that a wake-held datagram is re-sent until the service replies, so an app may process it twice; receive-only services can also sleep. Add the UDP replay and `--always-on` caveats here so users switching endpoints through deploy see these operational constraints.</violation>
</file>

<file name="src/commands/deploy.ts">

<violation number="1" location="src/commands/deploy.ts:38">
P2: An explicit `--endpoint http` is treated as “endpoint unchanged” here, and any returned `url` is ignored when `endpointHost` is present. Prefer the HTTP URL for `http`; reserve this fallback for an omitted endpoint.</violation>
</file>

<file name="src/commands/services.ts">

<violation number="1" location="src/commands/services.ts:148">
P3: The UDP/tcp+udp address can print `:undefined` or `:null` when the service response has a host but no port. Use the same `s.port ?? '?'` fallback as the no-host branch.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/index.ts
.option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>', 'port the app listens on (0 for a worker without a routed port)')
.option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
.option('--endpoint <http|tcp>', 'switch how the service is reached from this deploy on: tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
.option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: deploy --help enables UDP without warning that a wake-held datagram is re-sent until the service replies, so an app may process it twice; receive-only services can also sleep. Add the UDP replay and --always-on caveats here so users switching endpoints through deploy see these operational constraints.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/index.ts, line 553:

<comment>`deploy --help` enables UDP without warning that a wake-held datagram is re-sent until the service replies, so an app may process it twice; receive-only services can also sleep. Add the UDP replay and `--always-on` caveats here so users switching endpoints through deploy see these operational constraints.</comment>

<file context>
@@ -550,7 +550,7 @@ buildCmd.command('logs <build-id>').description('Read source-build output for a
   .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>', 'port the app listens on (0 for a worker without a routed port)')
   .option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
-  .option('--endpoint <http|tcp>', 'switch how the service is reached from this deploy on: tcp (raw TCP on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
+  .option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
   .option('--replace-source', 'the service deploys from a connected GitHub repo: switch it to this image and remove the repo connection (admin); without it such a deploy is refused')
   .option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
</file context>
Suggested change
.option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint')
.option('--endpoint <http|tcp|udp|tcp+udp>', 'switch how the service is reached from this deploy on: tcp / udp / tcp+udp (raw TCP, UDP or both on --port at its own public IPv6 address; IPv6 clients only) or http (back to its HTTPS domain). Omitted: keep the current endpoint. UDP wake-held datagrams are re-sent until the service first replies (an app may see them twice); receive-only UDP services need --always-on')

Comment thread src/commands/deploy.ts Outdated
// guessing tcp for what may be a udp service. Pure, so it's unit-tested.
export function deployedTarget(r: { url?: string; endpointHost?: string | null }, port: string | undefined, endpoint?: Endpoint): string {
if (r.endpointHost) {
if (endpoint === undefined || endpoint === 'http') return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: An explicit --endpoint http is treated as “endpoint unchanged” here, and any returned url is ignored when endpointHost is present. Prefer the HTTP URL for http; reserve this fallback for an omitted endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/deploy.ts, line 38:

<comment>An explicit `--endpoint http` is treated as “endpoint unchanged” here, and any returned `url` is ignored when `endpointHost` is present. Prefer the HTTP URL for `http`; reserve this fallback for an omitted endpoint.</comment>

<file context>
@@ -28,10 +28,16 @@ export function deployRequestBody(source: { image: string }, branch: string, opt
+// guessing tcp for what may be a udp service. Pure, so it's unit-tested.
+export function deployedTarget(r: { url?: string; endpointHost?: string | null }, port: string | undefined, endpoint?: Endpoint): string {
+  if (r.endpointHost) {
+    if (endpoint === undefined || endpoint === 'http') return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`
+    return (endpoint === 'tcp+udp' ? ['tcp', 'udp'] : [endpoint]).map((pr) => `${pr}://${r.endpointHost}:${port ?? '?'}`).join(', ')
+  }
</file context>
Suggested change
if (endpoint === undefined || endpoint === 'http') return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`
if (endpoint === undefined) return `${r.endpointHost}:${port ?? '?'} (endpoint unchanged; \`insta service list\` shows its protocol)`
if (endpoint === 'http') return r.url || `${r.endpointHost}:${port ?? '?'}`

Comment thread src/commands/services.ts
if (s.endpoint === 'tcp') return tcpAddress(s)
const protos = s.endpoint === 'tcp+udp' ? ['tcp', 'udp'] : ['udp']
if (!s.endpoint_host) return `${s.endpoint === 'tcp+udp' ? 'tcp+udp' : 'udp'}/${s.port ?? '?'} (address after the first deploy)`
return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port}`).join(', ')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The UDP/tcp+udp address can print :undefined or :null when the service response has a host but no port. Use the same s.port ?? '?' fallback as the no-host branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/commands/services.ts, line 148:

<comment>The UDP/tcp+udp address can print `:undefined` or `:null` when the service response has a host but no port. Use the same `s.port ?? '?'` fallback as the no-host branch.</comment>

<file context>
@@ -116,21 +116,38 @@ function defaultDatabasePort(type: string): number {
+  if (s.endpoint === 'tcp') return tcpAddress(s)
+  const protos = s.endpoint === 'tcp+udp' ? ['tcp', 'udp'] : ['udp']
+  if (!s.endpoint_host) return `${s.endpoint === 'tcp+udp' ? 'tcp+udp' : 'udp'}/${s.port ?? '?'} (address after the first deploy)`
+  return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port}`).join(', ')
+}
+
</file context>
Suggested change
return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port}`).join(', ')
return protos.map((pr) => `${pr}://${s.endpoint_host}:${s.port ?? '?'}`).join(', ')

…endpoint only as fallback

instacloud-platform#681 now returns `endpoint` beside `endpointHost` on
POST /deploy and on a live archive-deploy read, including a redeploy that
passed none. deployedTarget prints the scheme from it; the flag is the
fallback for an older platform, and the schemeless line only appears when
the result has a host but no endpoint. A plain redeploy of a tcp service
prints exactly `tcp://host:port` again. The archive outcome and its --json
carry the endpoint; an unknown value is treated as absent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

jwfing commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Review of f52d0077a98d9cfaa2ee5b8f160effca808e5dce.

Objectives and scope

Reviewed the six changed files against the PR contract, repository AGENTS.md/development guidance, and companion platform #681's request/result contract. Traced local validation, service creation/listing, image and archive deployment, endpoint precedence, JSON output, and compatibility fallbacks. Verified the local source copy against every tracked file's Git blob hash.

Critical

None detected within the review scope.

Suggestions

  • Include UDP operational caveats in deploy help. src/index.ts:553 enables switching an existing service to UDP, but its help omits the replay and receive-only-service caveats already present in service add --help. Users entering through deploy should also learn that wake-held datagrams may be delivered more than once and that receive-only UDP services need always-on enabled. Point to insta compute always-on on [service] for existing services, since deploy has no --always-on flag.

  • Link the command-reference companion before release. The inspected agent-facing reference still documents http|tcp for service creation and deployment. AGENTS.md rule 4 requires flag changes to be mirrored there. The PR acknowledges this follow-up; link its PR and coordinate publication so agents can discover UDP/tcp+udp and their operating constraints.

Information

The implementation keeps port policy on the platform, validates endpoint choices before deploy-side configuration/network work, and propagates normalized values through both deploy lanes. Result endpoint metadata takes precedence over the flag, while unknown/missing metadata uses the documented compatibility fallback. Existing HTTP/TCP rendering and JSON behavior have focused coverage. No additional reportable correctness, authorization, injection, credential-handling, dependency-change, or performance defect was detected in the inspected paths.

Release still depends on deploying platform #681, as the PR states; this review does not validate that upstream deployment.

Tests actually performed

  • Six focused files passed: endpoint-tcp, endpoint-udp, deploy-archive, deploy, deploy-port, and services: 157 tests passed.
  • Exact dependency installation was attempted both offline and online. It failed because required tarballs were uncached and npm registry DNS resolution returned EAI_AGAIN.
  • Focused tests used the installed Vitest/tool dependencies and genuine fflate 0.8.3 compiled locally from its tagged upstream source. This was not a clean lockfile installation.
  • Typecheck was attempted but did not pass in this incomplete dependency environment: commander, @clack/prompts, and dependency declarations were unavailable. A clean typecheck result remains unverified.
  • Full-suite, Windows/native-binary, and live-platform end-to-end checks were not run. Production source was not modified.

Verdict: approved — no Critical findings; the documentation suggestions are non-blocking.

Conflict: serviceListLine. Keeps the v6-lane address for tcp/udp/tcp+udp and
adds main's 'public off' and private address suffixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

jwfing commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Updated review of 3e0380e864d6c804cb3aaa89a0ce453ffe3f9df0.

Objectives and scope

Rechecked the UDP/tcp+udp contract, validation, request propagation through both deploy lanes, endpoint-result precedence, JSON output, and HTTP/TCP compatibility. Compared against the previously reviewed head: the additional changes merge main's private-network commands and service-list fields; the UDP implementation is unchanged. Checked those interactions against repository guidance and verified every tracked local file against the current head's Git blob hash.

Critical

None detected within the review scope.

Suggestions

The previous two non-blocking documentation suggestions remain:

  • Deploy help should include UDP wake replay and receive-only-service caveats. For an existing service, point to insta compute always-on on [service]; deploy itself has no --always-on flag.
  • Link and coordinate the command-reference companion before release. The freshly inspected agent-facing reference still lists only http|tcp, while AGENTS.md rule 4 requires flag changes to be mirrored there.

Information

Endpoint rendering preserves main's new public off and private-address fields. No additional reportable correctness, authorization, injection, credential-handling, architectural, dependency-change, or performance defect was detected in the inspected paths. Deployment of the companion platform API remains a release dependency.

Tests actually performed

  • Six focused repository files: 157 tests passed (endpoint-tcp, endpoint-udp, deploy-archive, deploy, deploy-port, services).
  • Three additional rendering checks passed: TCP, UDP and tcp+udp rows retain public/private status and suppress a stale HTTP domain.
  • Attempted private-network.test.ts: failed during import because this review environment lacks @clack/prompts; its tests did not execute.
  • Attempted typecheck: did not pass because dependencies/declarations remain incomplete, including @clack/prompts and commander. This is not a verified clean typecheck.

Limitations

Tests used reused installed tools and genuine fflate 0.8.3 compiled from its upstream tag, rather than a clean lockfile installation. Full-suite, help-process, Windows/native-binary and live-platform checks remain unverified. Production source was not modified.

Verdict: approved — no Critical findings; the documentation suggestions remain non-blocking.

@Fermionic-Lyu Fermionic-Lyu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, Approved.

@jwfing
jwfing merged commit 7a85ff2 into main Oct 16, 2026
3 checks passed
@jwfing jwfing mentioned this pull request Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants