From 21da58024bbdb47e8fb6a03b1312220d2cd172da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:37:42 +0000 Subject: [PATCH 1/3] build(deps): bump the github-actions group across 1 directory with 3 updates Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go) and [anchore/sbom-action/download-syft](https://github.com/anchore/sbom-action). Updates `actions/checkout` from 4.4.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.4.0...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `actions/setup-go` from 5.6.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/40f1582b2485089dde7abd97c1529aa768e1baff...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e) Updates `anchore/sbom-action/download-syft` from 0.24.2 to 0.24.3 - [Release notes](https://github.com/anchore/sbom-action/releases) - [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/sbom-action/compare/3ad7283483fc7af8ff2b4ea19663c2d5ca935e26...66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-go dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: anchore/sbom-action/download-syft dependency-version: 0.24.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/releaser.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89746f2..0f827e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,8 @@ jobs: race: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod # Same reason as the shared workflow: setup-go's cache tar corrupts @@ -58,8 +58,8 @@ jobs: nix: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod # Same reason as the race job above. diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index cfc6e03..3ebb3e7 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -9,8 +9,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod # go.mod pins a `toolchain` newer than the `go` line, so setup-go @@ -29,7 +29,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # ARCHITECTURE: the shared core release workflow runs goreleaser-action @@ -45,7 +45,7 @@ jobs: # first goreleaser-cross tag whose Go equals go.mod's `go` line. - name: Install Syft for archive SBOMs id: syft - uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 with: syft-version: v1.44.0 - name: Run CGO-capable GoReleaser From 35987a4a439b29352297387596b1b1f6f14a0696 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:48:13 -0400 Subject: [PATCH 2/3] fix(runtime): adopt Core authenticated native ownership --- .github/workflows/ci.yml | 2 +- AGENTS.md | 4 +- docs/native-qualification.md | 45 ++++++ go.mod | 31 ++-- go.sum | 74 +++++---- native_ownership_test.go | 302 +++++++++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+), 55 deletions(-) create mode 100644 docs/native-qualification.md create mode 100644 native_ownership_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f827e6..c441e45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: ci: - uses: codefly-dev/core/.github/workflows/go-service-ci.yml@v0.19.6 + uses: codefly-dev/core/.github/workflows/go-service-ci.yml@v0.19.10 with: # The Python runtime tests drive `uv` on the host (dependency sync, # pytest); it isn't on the runner by default. diff --git a/AGENTS.md b/AGENTS.md index 6a2310a..3205220 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ within reach. ## Build and test Derived from `.github/workflows/ci.yml`, which delegates to core's shared -`go-service-ci.yml`, pinned to the linked core's tag (`@v0.19.6`; move it with +`go-service-ci.yml`, pinned to the linked core's tag (`@v0.19.10`; move it with the core bump), and adds a `race` job of its own. Go comes from `go.mod` (1.27.3). All of it runs locally: @@ -197,4 +197,4 @@ carried here: each skill's frontmatter are asserted in `conformance_test.go`, so `go test ./...` enforces them with no workflow change. - There is no `CLAUDE.md`; if one is added, it is a one-line `@AGENTS.md` pointer. -- Treat this file as code: the PR that changes a process updates it. +- Treat this file as code; native ownership qualification: [procedure](docs/native-qualification.md). diff --git a/docs/native-qualification.md b/docs/native-qualification.md new file mode 100644 index 0000000..f179cf3 --- /dev/null +++ b/docs/native-qualification.md @@ -0,0 +1,45 @@ +# Native FastAPI ownership qualification + +FastAPI owns its uvicorn lifecycle; generic `service-python` supplies shared +Python tooling and retains its own no-op Runtime. Native FastAPI launches use +Core's `NewNativeEnvironment` and `NativeProc`, which owns authenticated process +group registration, birth checks and termination. Core v0.19.10 is the published +dependency; explicit orphan recovery remains Core's protocol consumed by CLI. +No registry, process matcher or recovery implementation belongs in this agent. + +Use the maintained `codefly agent deps --pin v0.19.10` operation to adopt the +published module, and `codefly agent build --native-only` to package/install the +agent and adjacent SBOM. Keep the manifest version unchanged for local builds. +Use a disposable OS `HOME` and `CODEFLY_HOME` for those commands; do not use an +incident or shared agent home. The installed binary is local qualification, +not evidence that a version bearing the same label was published or adopted. + +The opt-in gate consumes that installed binary and an existing CLI with +`recover process-group --preview/--expect`: + +```sh +FASTAPI_NATIVE_AGENT_HOME=/absolute/disposable/codefly-home \ +FASTAPI_QUALIFICATION_CLI=/absolute/qualified/codefly \ +GOWORK=off go test -count=1 -v -run '^TestNativeAgentOwnershipLifecycle$' . +``` + +The test creates a separate temporary OS home for registry authentication and +two disposable services. Core's authenticated manager starts each actual agent; +gRPC Load/Init/Start run real uvicorn workloads. Core resolves temporary network +mappings. Each workload reports its own PID and process group; Core authenticates +that group and its exact process birth. Ordinary Stop must remove the first +group and registration while the independent sibling birth and live owner stay +intact. FastAPI Stop releases its environment, so restart uses Init then Start. +At manager load the test captures the authenticated agent-group leader identity. +After restart it requires the current full owner identity to equal that original +birth before Core's identity-bound signal kills only the disposable agent owner. +The existing CLI previews that exact authenticated-v2 orphan, then consumes its +unchanged receipt to recover it. The sibling must still retain its exact birth +and live owner, and its ordinary Stop must also remove its registration. + +Record source revision/dirty state, module origin and sum, binary and SBOM hashes, +CLI identity, actual test output, and any skipped checks. The test does not prove +Linux operation, sandbox/principal policy, Docker, Nix, full CLI composition, +Wiki `model/model` adoption, or publication. Default tests skip this opt-in gate; +a default green run alone does not qualify an installed agent. Run container and +image-scanning checks only with the fleet's allocated Docker slot. diff --git a/go.mod b/go.mod index 341793d..13dd353 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codefly-dev/service-python-fastapi go 1.27.3 require ( - github.com/codefly-dev/core v0.19.7-0.20261025070021-a3bf508a5e55 + github.com/codefly-dev/core v0.19.10 github.com/codefly-dev/service-python v0.0.66 github.com/pelletier/go-toml/v2 v2.4.3 github.com/stretchr/testify v1.12.1 @@ -19,8 +19,8 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/semver v1.5.0 // 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 google.golang.org/protobuf v1.36.12 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 414a513..ede150e 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,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.7-0.20261025070021-a3bf508a5e55 h1:U9rzO6zSCigIUHuiiUTKMEtNzyYZ3e6il3A+sEv8jJk= -github.com/codefly-dev/core v0.19.7-0.20261025070021-a3bf508a5e55/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +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/codefly-dev/service-python v0.0.66 h1:Y74QvKAYMaOnZcsfRxuD9dpQo3//7Mjo12RYELyrp5o= @@ -53,8 +53,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG 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= @@ -115,8 +115,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= @@ -166,8 +166,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= @@ -194,8 +194,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= @@ -245,8 +243,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= @@ -307,14 +305,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= @@ -334,15 +332,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= @@ -355,20 +353,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..9de696c --- /dev/null +++ b/native_ownership_test.go @@ -0,0 +1,302 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "syscall" + "testing" + "time" + + "github.com/codefly-dev/core/agents/manager" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + 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("FASTAPI_NATIVE_AGENT_HOME") + cli := os.Getenv("FASTAPI_QUALIFICATION_CLI") + if agentHome == "" && cli == "" { + t.Skip("set FASTAPI_NATIVE_AGENT_HOME and FASTAPI_QUALIFICATION_CLI to qualify built artifacts") + } + if !filepath.IsAbs(agentHome) || !filepath.IsAbs(cli) { + t.Fatal("qualification inputs must be absolute paths") + } + t.Setenv("HOME", t.TempDir()) // 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(), 3*time.Minute) + defer cancel() + + a := startOwnedNativeService(t, ctx, "first") + b := startOwnedNativeService(t, ctx, "sibling") + assertNativeOwnership(t, ctx, a, true) + assertNativeOwnership(t, ctx, b, 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, true) + t.Log("ordinary Stop removed first workload; sibling remains authenticated and supervised") + + // Re-Init after Stop released the environment, then restart through the + // same Runtime and kill only its disposable owner + // birth so no agent Stop/finalizer can mask recovery of an orphan workload. + initialized, err := a.client.Init(ctx, a.init) + require.NoError(t, err) + require.Equal(t, runtimev0.InitStatus_READY, initialized.GetStatus().GetState(), initialized.GetStatus().GetMessage()) + startNativeWorkload(t, ctx, a) + owner, err := runners.InspectProcess(a.owner.PID) + if err != nil { + t.Fatal(err) + } + // The current PID must still be the original authenticated manager child. + // SignalProcess rechecks that identity at delivery; never select a new birth. + require.Equal(t, a.owner, owner, "agent owner birth changed before signaling") + t.Logf("owner pre-signal identity equals original: %+v", a.owner) + if err := runners.SignalProcess(ctx, a.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, false) + assertNativeOwnership(t, ctx, b, 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 || expected.Owner.BootID != owner.BootID || expected.Owner.StartID != owner.StartID { + t.Fatalf("actual agent did not write the expected v2 registration: %+v, %v", expected, err) + } + t.Logf("orphan preview: %s", preview) + assertNativeOwnership(t, ctx, a, 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, true) + 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 + conn *manager.AgentConn + client runtimev0.RuntimeClient + group *runners.TrackedProcessGroup + workload runners.ProcessIdentity + init *runtimev0.InitRequest + owner runners.ProcessIdentity +} + +func startOwnedNativeService(t *testing.T, ctx context.Context, name string) *ownedNativeService { + t.Helper() + workspace, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + writeNativeFixture(t, filepath.Join(workspace, "service.codefly.yaml"), fmt.Sprintf(`name: %s +version: 0.0.1 +agent: + publisher: codefly.dev + name: python-fastapi + version: %s +spec: + hot-reload: false +endpoints: + - name: http + api: http + visibility: private +`, name, agent.Version)) + writeNativeFixture(t, filepath.Join(workspace, "code", "pyproject.toml"), uvicornProject) + writeNativeFixture(t, filepath.Join(workspace, "code", "src", "main.py"), `import json, os +from pathlib import Path +Path("workload.json").write_text(json.dumps({"pid": os.getpid(), "pgid": os.getpgrp()})) +`+servingApp) + t.Setenv(runners.LaunchWorkspaceEnv, workspace) + conn, err := manager.Load(ctx, agent, manager.WithoutSandbox(), manager.WithoutPrincipal(), manager.WithWorkDir(workspace)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(conn.Close) + // The manager itself starts a tracked group. Capture its authenticated + // original leader while the managed connection is live, before any workload. + ownerGroup, err := runners.LookupProcessGroup(conn.ProcessInfo().PID) + require.NoError(t, err) + owner := ownerGroup.Leader() + require.Equal(t, conn.ProcessInfo().PID, owner.PID) + ownership, err := ownerGroup.InspectOwnership(ctx) + require.NoError(t, err) + require.True(t, ownership.Authenticated) + require.True(t, ownership.OwnerAlive) + require.Equal(t, workspace, ownership.LaunchWorkspace) + require.Contains(t, ownership.Members, owner) + current, err := runners.InspectProcess(owner.PID) + require.NoError(t, err) + require.Equal(t, owner, current) + t.Logf("captured authenticated agent owner: %+v", owner) + client := runtimev0.NewRuntimeClient(conn.GRPCConn()) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, _ = client.Stop(cleanup, &runtimev0.StopRequest{}) + }) + environment, err := resources.LocalEnvironment().Proto() + if err != nil { + t.Fatal(err) + } + loaded, err := client.Load(ctx, &runtimev0.LoadRequest{ + Identity: &basev0.ServiceIdentity{Name: name, Module: "qualification", Version: "0.0.1", WorkspacePath: workspace, RelativeToWorkspace: "."}, + Environment: environment, DisableCatch: true, + }) + if err != nil || loaded.GetStatus().GetState() != runtimev0.LoadStatus_READY { + t.Fatalf("load: %v, %v\n%s", loaded, err, conn.StderrTail()) + } + networkManager, err := network.NewRuntimeManager(ctx, nil) + require.NoError(t, err) + networkManager.WithTemporaryPorts() + identity := resources.ServiceIdentityFromProto(&basev0.ServiceIdentity{Name: name, Module: "qualification", Version: "0.0.1", WorkspacePath: workspace, RelativeToWorkspace: "."}) + mappings, err := networkManager.GenerateNetworkMappings(ctx, resources.LocalEnvironment(), &resources.Workspace{Name: "qualification"}, identity, loaded.GetEndpoints(), resources.NewRuntimeContextNative()) + require.NoError(t, err) + initRequest := &runtimev0.InitRequest{RuntimeContext: resources.NewRuntimeContextNative(), ProposedNetworkMappings: mappings} + initialized, err := client.Init(ctx, initRequest) + if err != nil || initialized.GetStatus().GetState() != runtimev0.InitStatus_READY { + t.Fatalf("init: %v, %v\n%s", initialized, err, conn.StderrTail()) + } + service := &ownedNativeService{workspace: workspace, conn: conn, client: client, init: initRequest, owner: owner} + startNativeWorkload(t, ctx, service) + return service +} + +func startNativeWorkload(t *testing.T, ctx context.Context, service *ownedNativeService) { + t.Helper() + pidFile := filepath.Join(service.workspace, "code", "workload.json") + if err := os.Remove(pidFile); err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatal(err) + } + started, err := service.client.Start(ctx, &runtimev0.StartRequest{}) + if err != nil || started.GetStatus().GetState() != runtimev0.StartStatus_STARTED { + t.Fatalf("start: %v, %v", started, err) + } + executable := started.GetStatus().GetStartedArtifact().GetExecutable() + if !filepath.IsAbs(executable) { + t.Fatalf("start did not report an absolute executable: %q", executable) + } + var witness struct { + PID int `json:"pid"` + PGID int `json:"pgid"` + } + waitNativeCondition(t, func() bool { + data, err := os.ReadFile(pidFile) + if err != nil { + return false + } + err = json.Unmarshal(data, &witness) + return err == nil && witness.PID > 1 && witness.PGID > 1 + }) + group, err := runners.LookupProcessGroup(witness.PGID) + if err != nil { + t.Fatal(err) + } + service.group = group + service.workload, err = runners.InspectProcess(witness.PID) + require.NoError(t, err) + assertNativeOwnership(t, ctx, service, true) + requireDeliverable(t, started.GetStatus().GetStartedArtifact()) + t.Logf("serving uvicorn birth: %+v; group leader: %+v; reported interpreter: %s", service.workload, group.Leader(), executable) + t.Cleanup(func() { + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = group.Terminate(cleanup) + _ = group.RemoveIfDeadContext(cleanup) + }) +} + +func assertNativeOwnership(t *testing.T, ctx context.Context, service *ownedNativeService, ownerAlive bool) { + t.Helper() + ownership, err := service.group.InspectOwnership(ctx) + require.NoError(t, err) + require.True(t, ownership.Authenticated) + require.Equal(t, ownerAlive, ownership.OwnerAlive) + require.Equal(t, service.workspace, ownership.LaunchWorkspace) + require.True(t, slices.Contains(ownership.Members, service.workload), "exact uvicorn birth absent: %+v", ownership) + birth, err := runners.InspectProcess(service.workload.PID) + require.NoError(t, err) + require.Equal(t, service.workload, birth) + if ownerAlive { + current, err := runners.InspectProcess(service.owner.PID) + require.NoError(t, err) + require.Equal(t, service.owner, current, "managed owner birth changed") + } +} + +func writeNativeFixture(t *testing.T, path, body string) { + t.Helper() + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) + require.NoError(t, os.WriteFile(path, []byte(body), 0600)) +} + +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 246a3e0f7bed72bdd4b158cb65cd745677504633 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:49:58 -0400 Subject: [PATCH 3/3] test(runtime): refuse stale owner before native signal --- docs/native-qualification.md | 2 ++ native_ownership_test.go | 36 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/native-qualification.md b/docs/native-qualification.md index f179cf3..e5cecb2 100644 --- a/docs/native-qualification.md +++ b/docs/native-qualification.md @@ -33,6 +33,8 @@ intact. FastAPI Stop releases its environment, so restart uses Init then Start. At manager load the test captures the authenticated agent-group leader identity. After restart it requires the current full owner identity to equal that original birth before Core's identity-bound signal kills only the disposable agent owner. +A stale-birth negative must be refused by that guard, with both original owners +and uvicorn workload births still alive, before the valid owner-death case. The existing CLI previews that exact authenticated-v2 orphan, then consumes its unchanged receipt to recover it. The sibling must still retain its exact birth and live owner, and its ordinary Stop must also remove its registration. diff --git a/native_ownership_test.go b/native_ownership_test.go index 9de696c..3daca55 100644 --- a/native_ownership_test.go +++ b/native_ownership_test.go @@ -77,17 +77,16 @@ func TestNativeAgentOwnershipLifecycle(t *testing.T) { require.NoError(t, err) require.Equal(t, runtimev0.InitStatus_READY, initialized.GetStatus().GetState(), initialized.GetStatus().GetMessage()) startNativeWorkload(t, ctx, a) - owner, err := runners.InspectProcess(a.owner.PID) - if err != nil { - t.Fatal(err) - } - // The current PID must still be the original authenticated manager child. - // SignalProcess rechecks that identity at delivery; never select a new birth. - require.Equal(t, a.owner, owner, "agent owner birth changed before signaling") - t.Logf("owner pre-signal identity equals original: %+v", a.owner) - if err := runners.SignalProcess(ctx, a.owner, syscall.SIGKILL); err != nil { - t.Fatal(err) - } + // A stale birth must be refused before SIGKILL delivery. If this guard + // delivered to the live PID, the exact owner and workload checks would fail. + stale := a.owner + stale.StartID++ + require.ErrorContains(t, signalNativeOwner(ctx, stale), "owner identity changed") + assertNativeOwnership(t, ctx, a, true) + assertNativeOwnership(t, ctx, b, true) + t.Log("stale owner refused with original agent and uvicorn births still alive") + require.NoError(t, signalNativeOwner(ctx, a.owner)) + t.Logf("signaled owner only after full identity matched original: %+v", a.owner) waitNativeCondition(t, func() bool { ownership, err := a.group.InspectOwnership(ctx) return err == nil && ownership.Authenticated && !ownership.OwnerAlive @@ -103,7 +102,7 @@ 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 || expected.Owner.BootID != owner.BootID || expected.Owner.StartID != owner.StartID { + if err != nil || expected.Namespace != "authenticated-v2" || expected.PGID != a.group.PGID() || expected.Owner.PID != a.owner.PID || expected.Owner.BootID != a.owner.BootID || expected.Owner.StartID != a.owner.StartID { t.Fatalf("actual agent did not write the expected v2 registration: %+v, %v", expected, err) } t.Logf("orphan preview: %s", preview) @@ -266,6 +265,19 @@ func startNativeWorkload(t *testing.T, ctx context.Context, service *ownedNative }) } +// signalNativeOwner is only the fixture's admission guard. Core owns delivery +// and rechecks the current full identity at the kernel-bound signal seam. +func signalNativeOwner(ctx context.Context, original runners.ProcessIdentity) error { + current, err := runners.InspectProcess(original.PID) + if err != nil { + return err + } + if current != original { + return fmt.Errorf("owner identity changed: original=%+v current=%+v", original, current) + } + return runners.SignalProcess(ctx, current, syscall.SIGKILL) +} + func assertNativeOwnership(t *testing.T, ctx context.Context, service *ownedNativeService, ownerAlive bool) { t.Helper() ownership, err := service.group.InspectOwnership(ctx)