From d3dcf8e1d8467a688f5cf62b32dcd9c4d8d0932b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:03:15 +0000 Subject: [PATCH 1/4] build(deps): bump github.com/codefly-dev/core Bumps the gomod group with 1 update: [github.com/codefly-dev/core](https://github.com/codefly-dev/core). Updates `github.com/codefly-dev/core` from 0.19.4 to 0.19.5 - [Release notes](https://github.com/codefly-dev/core/releases) - [Changelog](https://github.com/codefly-dev/core/blob/main/docs/releaseupdate.md) - [Commits](https://github.com/codefly-dev/core/compare/v0.19.4...v0.19.5) --- updated-dependencies: - dependency-name: github.com/codefly-dev/core dependency-version: 0.19.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gomod ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 303b576..d9889ff 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( github.com/Masterminds/semver v1.5.0 - github.com/codefly-dev/core v0.19.4 + github.com/codefly-dev/core v0.19.5 github.com/stretchr/testify v1.12.1 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 diff --git a/go.sum b/go.sum index 5bc496e..7ce4379 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= -github.com/codefly-dev/core v0.19.4 h1:vUvaFrWkB8B3ksQY/qlgBTVj3jMNhdEpJca8aofaals= -github.com/codefly-dev/core v0.19.4/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI= +github.com/codefly-dev/core v0.19.5 h1:Ikf4NjSPU995xvCjXRtvevgB9GHIBhGnFEVDz1XjewA= +github.com/codefly-dev/core v0.19.5/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From 296d42e9b9817cbd6bff88b41d44809812aed7e2 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:36:37 -0400 Subject: [PATCH 2/4] fix(runtime): adopt authenticated native ownership from Core v0.19.10 --- .claude/skills/verify-changes/SKILL.md | 41 ++++ go.mod | 31 +-- go.sum | 74 ++++--- native_ownership_test.go | 269 +++++++++++++++++++++++++ 4 files changed, 363 insertions(+), 52 deletions(-) create mode 100644 native_ownership_test.go diff --git a/.claude/skills/verify-changes/SKILL.md b/.claude/skills/verify-changes/SKILL.md index a1a27be..d2add83 100644 --- a/.claude/skills/verify-changes/SKILL.md +++ b/.claude/skills/verify-changes/SKILL.md @@ -86,6 +86,47 @@ source is the hack this repo's rules forbid. ## A skip is not a pass +### Actual native agent ownership + +Core v0.19.10 supplies the authenticated-v2 native writer and identity-bound +stop/recovery APIs. This agent consumes them through its existing native runner; +already-installed binaries retain the Core they were built with. + +After changing Core or the native lifecycle, build this checkout through +`codefly agent build --native-only` with a disposable OS `HOME` and +`CODEFLY_HOME`. Select the intended Core graph with `agent deps --pin` for a +published version; a source-adjacent `go.work` proves only that local source +graph and requires a packager that honours the CLI's prepared workfile. +Use an isolated source packager installation, never overwrite a shared agent. +Then qualify those exact installed bytes: + +```bash +SERVICE_NEXTJS_NATIVE_AGENT_HOME=/absolute/disposable/codefly-home \ +SERVICE_NEXTJS_QUALIFICATION_CLI=/absolute/reviewed/codefly \ + go test -run '^TestNativeAgentOwnershipLifecycle$' -count=1 -v . +``` + +This opt-in gate loads the installed agent through Core's authenticated manager +and drives real Next.js applications through gRPC Load/Init/Start/Stop/restart. +It checks that Start's RPC lifetime does not own the workload, ordinary Stop +removes the registered group, and the actual writer records authenticated-v2 +ownership. It kills only the exact disposable agent owner, recovers its orphan +through CLI preview/expect, and verifies an independently launched sibling's +process births remain unchanged. Core owns authentication, process inspection, +signalling and registry removal; this agent adds no recovery mechanism. + +The test isolates OS HOME (Core's registry) from the supplied installation home. +It needs npm, Node, network access for the fixture's pinned dependencies, and a +CLI implementing the reviewed recovery contract. It bypasses sandbox/principal +policy explicitly. It does not qualify Linux when run on macOS, Docker, Nix, +full CLI composition, or a published release. Record binary/SBOM/CLI hashes and +embedded Core metadata with the result; a local manifest version is not proof +of publication. Missing opt-in inputs produce a skip, never qualification. +The ownership proof covers the tracked npm process group. Next.js 16.4.1 can +briefly start a detached telemetry flush helper even with telemetry disabled; +that helper is outside this group contract. Fixture-home deletion retries handle +its filesystem race without claiming or signalling an unrelated process. + These exclude themselves and report success: | Test | Runs only when | diff --git a/go.mod b/go.mod index d9889ff..eaeeffd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( github.com/Masterminds/semver v1.5.0 - github.com/codefly-dev/core v0.19.5 + github.com/codefly-dev/core v0.19.10 github.com/stretchr/testify v1.12.1 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 @@ -19,8 +19,8 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.4.1 // indirect + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect + github.com/ProtonMail/go-crypto v1.5.2 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -32,6 +32,7 @@ require ( github.com/codefly-dev/gortk v0.2.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.2.0 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dekobon/tree-sitter-groovy v0.3.0 // indirect @@ -56,7 +57,7 @@ require ( github.com/go-openapi/jsonreference v1.0.2 // indirect github.com/go-openapi/loads v0.25.3 // indirect github.com/go-openapi/spec v1.0.1 // indirect - github.com/go-openapi/strfmt v0.27.2 // indirect + github.com/go-openapi/strfmt v0.27.3 // indirect github.com/go-openapi/swag v0.29.2 // indirect github.com/go-openapi/swag/cmdutils v0.29.2 // indirect github.com/go-openapi/swag/conv v0.29.2 // indirect @@ -85,10 +86,11 @@ require ( github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-pointer v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.28 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.1.0 // indirect github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -119,22 +121,23 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 // indirect go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect go.opentelemetry.io/otel/log v1.47.0 // indirect go.opentelemetry.io/otel/metric v1.47.0 // indirect go.opentelemetry.io/otel/trace v1.47.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect - golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.60.0 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect golang.org/x/sys v0.49.0 // indirect - golang.org/x/term v0.46.0 // indirect - golang.org/x/text v0.42.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect diff --git a/go.sum b/go.sum index 7ce4379..8a0ab7f 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,10 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= -github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/ProtonMail/go-crypto v1.5.2 h1:cucYnvqcY7UOXVD//mSyjeaPY0SSN3v5cDkYPxumINk= +github.com/ProtonMail/go-crypto v1.5.2/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -43,16 +43,16 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= -github.com/codefly-dev/core v0.19.5 h1:Ikf4NjSPU995xvCjXRtvevgB9GHIBhGnFEVDz1XjewA= -github.com/codefly-dev/core v0.19.5/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI= +github.com/codefly-dev/core v0.19.10 h1:rTeh1vph0xGtpiCCt2Dl1AH2Rz9hzIYt1YGcbv6bLYE= +github.com/codefly-dev/core v0.19.10/go.mod h1:aTJY3Rw0OSVkbWRThgDzSkqieoF9mW4MiPRXM8Vzxww= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= +github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -113,8 +113,8 @@ github.com/go-openapi/loads v0.25.3 h1:V+jKy/thXWdLJUuYC8sZX2dICyAa8M3DokF70jj34 github.com/go-openapi/loads v0.25.3/go.mod h1:LgLyCSOLBL2Qnj0Ps1oo2YH8jZoEKsMeVZCc+ALIxFo= github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= -github.com/go-openapi/strfmt v0.27.2 h1:SG32SlbwNy92s0KJiVxt2joJeFdqIYHvwrA0OU6HqzQ= -github.com/go-openapi/strfmt v0.27.2/go.mod h1:M4CKsMO0Fb8qR10+1Ra75wCKNNquy+Vj+4LWZrhTo2E= +github.com/go-openapi/strfmt v0.27.3 h1:cyf4J5Wjpd6bFpAs44MF8PsbWF/mWXhn82Ck/hgQZgw= +github.com/go-openapi/strfmt v0.27.3/go.mod h1:Bl+xadPLa3nhBiaCAxNND1vrQfzyEjF0vBrLx96n/4E= github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= @@ -164,8 +164,8 @@ github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfh github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 h1:QMILzF8BA7xftE4zVORkRrlv08m12St3csK4RsyRgRs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0/go.mod h1:+ZQ6iUL8+ATyxGDQNuRKpNwJFdGDUi0e7E1QL9MvI7U= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -192,8 +192,6 @@ github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= @@ -241,8 +239,8 @@ github.com/shoenig/go-m1cpu v0.2.2/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+l github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -303,14 +301,14 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 h1:Tq+E65HaNbZTeqcLw6QUhsV+/EIWdkfUH2CQzcrlE5M= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0/go.mod h1:A+EVhDakAj1waJBPhmTszZaSr08ww8MRRKy1nB1rqck= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw= go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= @@ -330,15 +328,15 @@ go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= +golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -352,20 +350,20 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= -google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941 h1:DS/w3N8rsfrwxwgGSj6WhMsPo7aNQGmDx74J81zd0NQ= +google.golang.org/genproto/googleapis/api v0.0.0-20261012220534-4341edf3f941/go.mod h1:AuGAdi9KnXss7xbLxIKhCvk+Ro/SxpET/iS45DNYN+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= diff --git a/native_ownership_test.go b/native_ownership_test.go new file mode 100644 index 0000000..0a67915 --- /dev/null +++ b/native_ownership_test.go @@ -0,0 +1,269 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/json" + "errors" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "github.com/codefly-dev/core/agents/manager" + runtimev0 "github.com/codefly-dev/core/generated/go/codefly/services/runtime/v0" + "github.com/codefly-dev/core/network" + "github.com/codefly-dev/core/resources" + runners "github.com/codefly-dev/core/runners/base" + "github.com/stretchr/testify/require" +) + +// This opt-in gate consumes the actual `agent build --native-only` output. +// It never builds a substitute agent, touches the user's registry, or selects +// processes by name. Both services and their agent owners are disposable. +func TestNativeAgentOwnershipLifecycle(t *testing.T) { + agentHome := os.Getenv("SERVICE_NEXTJS_NATIVE_AGENT_HOME") + cli := os.Getenv("SERVICE_NEXTJS_QUALIFICATION_CLI") + if agentHome == "" && cli == "" { + t.Skip("set SERVICE_NEXTJS_NATIVE_AGENT_HOME and SERVICE_NEXTJS_QUALIFICATION_CLI to qualify built artifacts") + } + if !filepath.IsAbs(agentHome) || !filepath.IsAbs(cli) { + t.Fatal("qualification inputs must be absolute paths") + } + // Next's disabled telemetry still starts a short-lived detached flush + // helper at shutdown. It can race filesystem cleanup after the tracked + // npm group is dead. Retry deletion of our fixture home only; this is not + // permission to discover or signal processes outside that group. + home, err := os.MkdirTemp("", "nextjs-native-home-") + require.NoError(t, err) + t.Cleanup(func() { + require.Eventually(t, func() bool { return os.RemoveAll(home) == nil }, 5*time.Second, 25*time.Millisecond) + }) + t.Setenv("HOME", home) // Core's process registry is OS-home scoped. + t.Setenv(resources.CodeflyHomeEnv, agentHome) + t.Setenv("GOWORK", "off") + installed, err := agent.Path(t.Context()) + if err != nil { + t.Fatal(err) + } + for _, path := range []string{installed, installed + ".cdx.json", cli} { + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + t.Logf("artifact %s sha256=%x", path, sha256.Sum256(body)) + } + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) + defer cancel() + + a := startOwnedNativeService(t, ctx) + b := startOwnedNativeService(t, ctx) + require.NotEqual(t, a.conn.ProcessInfo().PID, b.conn.ProcessInfo().PID, "sibling must have an independent agent owner") + siblingBefore, err := b.group.InspectOwnership(ctx) + require.NoError(t, err) + assertNativeOwnership(t, ctx, a.group, a.workspace, true) + assertNativeOwnership(t, ctx, b.group, b.workspace, true) + + // Ordinary Runtime.Stop must retire A without affecting B's exact birth. + stopped, err := a.client.Stop(ctx, &runtimev0.StopRequest{}) + if err != nil || stopped.GetStatus().GetState() != runtimev0.StopStatus_SUCCESS { + t.Fatalf("ordinary stop: %v, %v", stopped, err) + } + waitNativeCondition(t, func() bool { return !a.group.Alive() }) + if _, err := runners.LookupProcessGroup(a.group.PGID()); !errors.Is(err, runners.ErrProcessGroupNotRegistered) { + t.Fatalf("ordinary stop retained the workload registration: %v", err) + } + assertNativeOwnership(t, ctx, b.group, b.workspace, true) + t.Log("ordinary Stop removed first workload; sibling remains authenticated and supervised") + + // Restart through the same Runtime, then kill only its disposable owner + // birth so no agent Stop/finalizer can mask recovery of an orphan workload. + a.group = startNativeWorkload(t, ctx, a.client, a.workspace, a.address) + owner, err := runners.InspectProcess(a.conn.ProcessInfo().PID) + if err != nil { + t.Fatal(err) + } + if err := runners.SignalProcess(ctx, owner, syscall.SIGKILL); err != nil { + t.Fatal(err) + } + waitNativeCondition(t, func() bool { + ownership, err := a.group.InspectOwnership(ctx) + return err == nil && ownership.Authenticated && !ownership.OwnerAlive + }) + assertNativeOwnership(t, ctx, a.group, a.workspace, false) + assertNativeOwnership(t, ctx, b.group, b.workspace, true) + + // Use the maintained CLI consumer and Core's strict receipt parser; do not + // invent a recovery selection from registry JSON or a list of descendants. + pgid := strconv.Itoa(a.group.PGID()) + preview, err := exec.CommandContext(ctx, cli, "recover", "process-group", pgid, "--preview").Output() + if err != nil { + t.Fatalf("preview: %v", err) + } + expected, err := runners.ParseProcessGroupRecoveryExpectation(preview) + if err != nil || expected.Namespace != "authenticated-v2" || expected.PGID != a.group.PGID() || expected.Owner.PID != owner.PID { + t.Fatalf("actual agent did not write the expected v2 registration: %+v, %v", expected, err) + } + require.Equal(t, owner.BootID, expected.Owner.BootID) + require.Equal(t, owner.StartID, expected.Owner.StartID) + t.Logf("orphan preview: %s", preview) + assertNativeOwnership(t, ctx, a.group, a.workspace, false) + receipt := filepath.Join(t.TempDir(), "recovery.json") + if err := os.WriteFile(receipt, preview, 0600); err != nil { + t.Fatal(err) + } + result, err := exec.CommandContext(ctx, cli, "recover", "process-group", pgid, "--expect", receipt).CombinedOutput() + if err != nil { + t.Fatalf("recover: %v\n%s", err, result) + } + if a.group.Alive() { + t.Fatal("recovery left the selected workload alive") + } + if _, err := runners.LookupProcessGroup(a.group.PGID()); !errors.Is(err, runners.ErrProcessGroupNotRegistered) { + t.Fatalf("recovery did not remove the exact registration: %v", err) + } + assertNativeOwnership(t, ctx, b.group, b.workspace, true) + siblingAfter, err := b.group.InspectOwnership(ctx) + require.NoError(t, err) + require.Equal(t, siblingBefore.Members, siblingAfter.Members, "sibling process births changed during recovery") + t.Logf("recovery result: %s; sibling preserved", result) + stopped, err = b.client.Stop(ctx, &runtimev0.StopRequest{}) + if err != nil || stopped.GetStatus().GetState() != runtimev0.StopStatus_SUCCESS { + t.Fatalf("sibling ordinary stop: %v, %v", stopped, err) + } + waitNativeCondition(t, func() bool { return !b.group.Alive() }) + _, err = runners.LookupProcessGroup(b.group.PGID()) + require.ErrorIs(t, err, runners.ErrProcessGroupNotRegistered) +} + +type ownedNativeService struct { + workspace string + address string + conn *manager.AgentConn + client runtimev0.RuntimeClient + group *runners.TrackedProcessGroup +} + +func startOwnedNativeService(t *testing.T, ctx context.Context) *ownedNativeService { + t.Helper() + workspace, err := filepath.EvalSymlinks(t.TempDir()) + require.NoError(t, err) + identity, environment := testIdentity(t, workspace) + serviceDir := filepath.Join(workspace, identity.RelativeToWorkspace) + writeProductionTestFile(t, serviceDir, "service.codefly.yaml", "name: frontend\nversion: 0.0.0\nendpoints:\n - name: http\n api: http\n") + // The shipped application owns these exact framework pins. Exercise real + // Next.js and npm (including their child processes), not a stand-in runner. + manifest, err := os.ReadFile("base/code/package.json") + require.NoError(t, err) + var base struct { + Dependencies map[string]string `json:"dependencies"` + } + require.NoError(t, json.Unmarshal(manifest, &base)) + dependencies := map[string]string{} + for _, name := range []string{"next", "react", "react-dom"} { + dependencies[name] = base.Dependencies[name] + } + fixture, err := json.Marshal(map[string]any{ + "name": "native-ownership", "private": true, + "scripts": map[string]string{"dev": "next dev --hostname 127.0.0.1"}, + "dependencies": dependencies, + }) + require.NoError(t, err) + writeProductionTestFile(t, serviceDir, "code/package.json", string(fixture)) + writeProductionTestFile(t, serviceDir, "code/pages/index.js", "export default function Home() { return 'native ownership'; }\n") + writeProductionTestFile(t, serviceDir, "code/pages/api/pid.js", "export default function handler(req, res) { res.json({pid: process.pid}); }\n") + t.Setenv(runners.LaunchWorkspaceEnv, workspace) + conn, err := manager.Load(ctx, agent, manager.WithoutSandbox(), manager.WithoutPrincipal(), manager.WithWorkDir(workspace)) + require.NoError(t, err) + t.Cleanup(conn.Close) + client := runtimev0.NewRuntimeClient(conn.GRPCConn()) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, _ = client.Stop(cleanup, &runtimev0.StopRequest{}) + }) + environmentProto, err := environment.Proto() + require.NoError(t, err) + loaded, err := client.Load(ctx, &runtimev0.LoadRequest{Identity: identity, Environment: environmentProto, DisableCatch: true}) + if err != nil || loaded.GetStatus().GetState() != runtimev0.LoadStatus_READY { + t.Fatalf("load: %v, %v\n%s", loaded, err, conn.StderrTail()) + } + composition, err := resources.LoadWorkspaceFromDir(ctx, workspace) + require.NoError(t, err) + ports, err := network.NewRuntimeManager(ctx, nil) + require.NoError(t, err) + ports.WithTemporaryPorts() + mappings, err := ports.GenerateNetworkMappings(ctx, environment, composition, resources.ServiceIdentityFromProto(identity), loaded.GetEndpoints(), resources.NewRuntimeContextNative()) + require.NoError(t, err) + endpoint, err := resources.FindHTTPEndpoint(ctx, loaded.GetEndpoints()) + require.NoError(t, err) + instance, err := resources.FindNetworkInstanceInNetworkMappings(ctx, mappings, endpoint, resources.NewNativeNetworkAccess()) + require.NoError(t, err) + initialized, err := client.Init(ctx, &runtimev0.InitRequest{RuntimeContext: resources.NewRuntimeContextNative(), ProposedNetworkMappings: mappings}) + if err != nil || initialized.GetStatus().GetState() != runtimev0.InitStatus_READY { + t.Fatalf("init: %v, %v\n%s", initialized, err, conn.StderrTail()) + } + return &ownedNativeService{workspace: workspace, address: instance.Address, conn: conn, client: client, + group: startNativeWorkload(t, ctx, client, workspace, instance.Address)} +} + +func startNativeWorkload(t *testing.T, ctx context.Context, client runtimev0.RuntimeClient, workspace, address string) *runners.TrackedProcessGroup { + t.Helper() + // Returning from (and cancelling) Start's RPC must not end the workload. + startCtx, cancel := context.WithCancel(ctx) + started, err := client.Start(startCtx, &runtimev0.StartRequest{}) + cancel() + if err != nil || started.GetStatus().GetState() != runtimev0.StartStatus_STARTED { + t.Fatalf("start: %v, %v", started, err) + } + httpClient := &http.Client{Timeout: 10 * time.Second} + response, err := httpClient.Get(strings.TrimRight(address, "/") + "/api/pid") + require.NoError(t, err) + defer response.Body.Close() + require.Equal(t, http.StatusOK, response.StatusCode) + var workload struct { + PID int `json:"pid"` + } + require.NoError(t, json.NewDecoder(response.Body).Decode(&workload)) + identity, err := runners.InspectProcess(workload.PID) + require.NoError(t, err) + group, err := runners.LookupProcessGroup(identity.PGID) + require.NoError(t, err) + t.Logf("registered leader %+v", group.Leader()) + assertNativeOwnership(t, ctx, group, workspace, true) + ownership, err := group.InspectOwnership(ctx) + require.NoError(t, err) + require.Contains(t, ownership.Members, identity) + t.Logf("Next.js HTTP process %+v in authenticated group %d", identity, group.PGID()) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = group.Terminate(cleanup) + _ = group.RemoveIfDeadContext(cleanup) + }) + return group +} + +func assertNativeOwnership(t *testing.T, ctx context.Context, group *runners.TrackedProcessGroup, workspace string, ownerAlive bool) { + t.Helper() + ownership, err := group.InspectOwnership(ctx) + if err != nil || !ownership.Authenticated || ownership.OwnerAlive != ownerAlive || ownership.LaunchWorkspace != workspace || len(ownership.Members) == 0 { + t.Fatalf("ownership for %d (registered leader %+v): %+v, %v", group.PGID(), group.Leader(), ownership, err) + } +} + +func waitNativeCondition(t *testing.T, check func() bool) { + t.Helper() + deadline := time.Now().Add(10 * time.Second) + for !check() { + if time.Now().After(deadline) { + t.Fatal("native lifecycle postcondition did not arrive") + } + time.Sleep(20 * time.Millisecond) + } +} From 8c9a44fb81fdda15fdaa96439879aafa2a90088a Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:50:44 -0400 Subject: [PATCH 3/4] test(runtime): authenticate original agent birth before owner death --- .claude/skills/verify-changes/SKILL.md | 4 ++ native_ownership_test.go | 73 ++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/.claude/skills/verify-changes/SKILL.md b/.claude/skills/verify-changes/SKILL.md index d2add83..c7de4c2 100644 --- a/.claude/skills/verify-changes/SKILL.md +++ b/.claude/skills/verify-changes/SKILL.md @@ -114,6 +114,10 @@ ownership. It kills only the exact disposable agent owner, recovers its orphan through CLI preview/expect, and verifies an independently launched sibling's process births remain unchanged. Core owns authentication, process inspection, signalling and registry removal; this agent adds no recovery mechanism. +The test retains the manager's authenticated original launch identity before +workload actions, compares the current full identity before signaling, and binds +the later recovery receipt to that retained birth. Its stale-owner regression +uses a real disposable process and requires refusal without SIGKILL delivery. The test isolates OS HOME (Core's registry) from the supplied installation home. It needs npm, Node, network access for the fixture's pinned dependencies, and a diff --git a/native_ownership_test.go b/native_ownership_test.go index 0a67915..cdf6ba4 100644 --- a/native_ownership_test.go +++ b/native_ownership_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "errors" + "fmt" "net/http" "os" "os/exec" @@ -84,11 +85,7 @@ func TestNativeAgentOwnershipLifecycle(t *testing.T) { // Restart through the same Runtime, then kill only its disposable owner // birth so no agent Stop/finalizer can mask recovery of an orphan workload. a.group = startNativeWorkload(t, ctx, a.client, a.workspace, a.address) - owner, err := runners.InspectProcess(a.conn.ProcessInfo().PID) - if err != nil { - t.Fatal(err) - } - if err := runners.SignalProcess(ctx, owner, syscall.SIGKILL); err != nil { + if err := killNativeOwner(ctx, a.owner); err != nil { t.Fatal(err) } waitNativeCondition(t, func() bool { @@ -106,11 +103,13 @@ func TestNativeAgentOwnershipLifecycle(t *testing.T) { t.Fatalf("preview: %v", err) } expected, err := runners.ParseProcessGroupRecoveryExpectation(preview) - if err != nil || expected.Namespace != "authenticated-v2" || expected.PGID != a.group.PGID() || expected.Owner.PID != owner.PID { + if err != nil || expected.Namespace != "authenticated-v2" || expected.PGID != a.group.PGID() || expected.Owner.PID != a.owner.PID { t.Fatalf("actual agent did not write the expected v2 registration: %+v, %v", expected, err) } - require.Equal(t, owner.BootID, expected.Owner.BootID) - require.Equal(t, owner.StartID, expected.Owner.StartID) + // Recovery represents the recorded owner with PGID=0; compare its birth, + // not the full struct, to the launch identity retained before any workload. + require.Equal(t, a.owner.BootID, expected.Owner.BootID) + require.Equal(t, a.owner.StartID, expected.Owner.StartID) t.Logf("orphan preview: %s", preview) assertNativeOwnership(t, ctx, a.group, a.workspace, false) receipt := filepath.Join(t.TempDir(), "recovery.json") @@ -131,6 +130,9 @@ func TestNativeAgentOwnershipLifecycle(t *testing.T) { siblingAfter, err := b.group.InspectOwnership(ctx) require.NoError(t, err) require.Equal(t, siblingBefore.Members, siblingAfter.Members, "sibling process births changed during recovery") + siblingOwner, err := runners.InspectProcess(b.owner.PID) + require.NoError(t, err) + require.Equal(t, b.owner, siblingOwner, "sibling's original agent birth changed") t.Logf("recovery result: %s; sibling preserved", result) stopped, err = b.client.Stop(ctx, &runtimev0.StopRequest{}) if err != nil || stopped.GetStatus().GetState() != runtimev0.StopStatus_SUCCESS { @@ -145,6 +147,7 @@ type ownedNativeService struct { workspace string address string conn *manager.AgentConn + owner runners.ProcessIdentity client runtimev0.RuntimeClient group *runners.TrackedProcessGroup } @@ -181,6 +184,19 @@ func startOwnedNativeService(t *testing.T, ctx context.Context) *ownedNativeServ conn, err := manager.Load(ctx, agent, manager.WithoutSandbox(), manager.WithoutPrincipal(), manager.WithWorkDir(workspace)) require.NoError(t, err) t.Cleanup(conn.Close) + // Retain the manager's authenticated launch record before Load/Init/Start. + // A later PID lookup alone cannot establish which birth we launched. + ownerGroup, err := runners.LookupProcessGroup(conn.ProcessInfo().PID) + require.NoError(t, err) + owner := ownerGroup.Leader() + require.Equal(t, conn.ProcessInfo().PID, owner.PID) + ownerObservation, err := ownerGroup.InspectOwnership(ctx) + require.NoError(t, err) + require.True(t, ownerObservation.Authenticated) + require.True(t, ownerObservation.OwnerAlive) + require.Equal(t, workspace, ownerObservation.LaunchWorkspace) + require.Contains(t, ownerObservation.Members, owner) + t.Logf("authenticated original agent launch %+v", owner) client := runtimev0.NewRuntimeClient(conn.GRPCConn()) t.Cleanup(func() { cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -208,10 +224,49 @@ func startOwnedNativeService(t *testing.T, ctx context.Context) *ownedNativeServ if err != nil || initialized.GetStatus().GetState() != runtimev0.InitStatus_READY { t.Fatalf("init: %v, %v\n%s", initialized, err, conn.StderrTail()) } - return &ownedNativeService{workspace: workspace, address: instance.Address, conn: conn, client: client, + return &ownedNativeService{workspace: workspace, address: instance.Address, conn: conn, owner: owner, client: client, group: startNativeWorkload(t, ctx, client, workspace, instance.Address)} } +// This fixture never execs or changes process groups. Require its complete +// retained launch identity before asking Core to bind delivery to that birth. +func killNativeOwner(ctx context.Context, original runners.ProcessIdentity) error { + current, err := runners.InspectProcess(original.PID) + if err != nil { + return err + } + if current != original { + return fmt.Errorf("original native owner changed: %w", runners.ErrProcessNotFound) + } + return runners.SignalProcess(ctx, current, syscall.SIGKILL) +} + +func TestNativeOwnerSignalRejectsStaleBirth(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + cmd := exec.Command("sleep", "30") + group, err := runners.StartTrackedProcessGroup(cmd) + require.NoError(t, err) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = group.Terminate(cleanup) + _ = cmd.Wait() + _ = group.RemoveIfDeadContext(cleanup) + }) + original := group.Leader() + observation, err := group.InspectOwnership(t.Context()) + require.NoError(t, err) + require.True(t, observation.Authenticated) + require.True(t, observation.OwnerAlive) + require.Contains(t, observation.Members, original) + stale := original + stale.StartID++ + require.ErrorIs(t, killNativeOwner(t.Context(), stale), runners.ErrProcessNotFound) + current, err := runners.InspectProcess(original.PID) + require.NoError(t, err, "stale owner selection delivered SIGKILL") + require.Equal(t, original, current, "original process must survive stale-owner refusal unchanged") +} + func startNativeWorkload(t *testing.T, ctx context.Context, client runtimev0.RuntimeClient, workspace, address string) *runners.TrackedProcessGroup { t.Helper() // Returning from (and cancelling) Start's RPC must not end the workload. From 6a02740267a49f77de7cbbe668c6070771ba85ee Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:14:44 -0400 Subject: [PATCH 4/4] test(runtime): wait for the retained fixture birth to finish exec --- .claude/skills/verify-changes/SKILL.md | 3 ++ native_ownership_test.go | 42 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.claude/skills/verify-changes/SKILL.md b/.claude/skills/verify-changes/SKILL.md index c7de4c2..0952eb2 100644 --- a/.claude/skills/verify-changes/SKILL.md +++ b/.claude/skills/verify-changes/SKILL.md @@ -118,6 +118,9 @@ The test retains the manager's authenticated original launch identity before workload actions, compares the current full identity before signaling, and binds the later recovery receipt to that retained birth. Its stale-owner regression uses a real disposable process and requires refusal without SIGKILL delivery. +That standalone fixture waits for Core's launch gate to exec the target while +retaining the original birth; missing/replaced ownership or cancellation fails +readiness, and the bounded wait cannot turn a timeout into qualification. The test isolates OS HOME (Core's registry) from the supplied installation home. It needs npm, Node, network access for the fixture's pinned dependencies, and a diff --git a/native_ownership_test.go b/native_ownership_test.go index cdf6ba4..c6aabf2 100644 --- a/native_ownership_test.go +++ b/native_ownership_test.go @@ -241,6 +241,30 @@ func killNativeOwner(ctx context.Context, original runners.ProcessIdentity) erro return runners.SignalProcess(ctx, current, syscall.SIGKILL) } +// Core publishes the original birth before releasing its shell launch gate; +// returning from StartTrackedProcessGroup does not mean the target has execed. +// Wait only for that retained birth's expected execution state, never adopt a +// newly observed identity. The manager-loaded agent already has its handshake. +func nativeOwnerReady(ctx context.Context, group *runners.TrackedProcessGroup, original runners.ProcessIdentity) (bool, error) { + observation, err := group.InspectOwnership(ctx) + if err != nil { + return false, err + } + if !observation.Authenticated || !observation.OwnerAlive { + return false, runners.ErrProcessNotFound + } + for _, member := range observation.Members { + if member.PID != original.PID { + continue + } + if member.PGID != original.PGID || member.BootID != original.BootID || member.StartID != original.StartID { + return false, runners.ErrProcessNotFound + } + return member == original, nil + } + return false, runners.ErrProcessNotFound +} + func TestNativeOwnerSignalRejectsStaleBirth(t *testing.T) { t.Setenv("HOME", t.TempDir()) cmd := exec.Command("sleep", "30") @@ -254,13 +278,21 @@ func TestNativeOwnerSignalRejectsStaleBirth(t *testing.T) { _ = group.RemoveIfDeadContext(cleanup) }) original := group.Leader() - observation, err := group.InspectOwnership(t.Context()) - require.NoError(t, err) - require.True(t, observation.Authenticated) - require.True(t, observation.OwnerAlive) - require.Contains(t, observation.Members, original) + waitNativeCondition(t, func() bool { + ready, err := nativeOwnerReady(t.Context(), group, original) + require.NoError(t, err) + return ready + }) stale := original stale.StartID++ + ready, err := nativeOwnerReady(t.Context(), group, stale) + require.ErrorIs(t, err, runners.ErrProcessNotFound) + require.False(t, ready, "readiness must not adopt a replacement birth") + cancelled, cancel := context.WithCancel(t.Context()) + cancel() + ready, err = nativeOwnerReady(cancelled, group, original) + require.ErrorIs(t, err, context.Canceled) + require.False(t, ready, "cancelled readiness must not pass") require.ErrorIs(t, killNativeOwner(t.Context(), stale), runners.ErrProcessNotFound) current, err := runners.InspectProcess(original.PID) require.NoError(t, err, "stale owner selection delivered SIGKILL")