Skip to content

refactor(contract): a wrapper prologue is data, not two hand-written lists - #502

Merged
defangdevs merged 1 commit into
masterfrom
feat/wrapper-prologue-contract
Sep 2, 2026
Merged

refactor(contract): a wrapper prologue is data, not two hand-written lists#502
defangdevs merged 1 commit into
masterfrom
feat/wrapper-prologue-contract

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

#500 added a fourth parity comparison because a wrapper prologue could diverge. This removes the ability to diverge: neither backend writes one any more. modules/src/contract/wrappers.json declares each generated wrapper's environment once — the export, and the reason it exists — and both renderers render that.

Why a wrapper, specifically

The contract mechanism is issue #451's, one level over. It already carries a unit's bindings as data both modules/agent-box.nix.in and bin/agentbox read. A wrapper belongs to no unit, which is exactly why it fell through: the webhook receiver and the settings daemon both run agent-box-webhook-spawn, so what its payload gets is the block above the exec, not any unit's Environment=.

The schema

kind means resolved by
bin names a PROGRAM, never a path contractPrograms here, self.bin/<program> natively — the same resolution a unit contract already uses
config names a per-box VALUE by key each backend's own configuration

config names a key rather than a value because the two backends may legitimately resolve one to different text: a diagnostic that says where the fleet-wide default LIVES has to name a NixOS option on one and config.yaml on the other. omitWhenEmpty drops the line when the value is empty, so "nothing set" stays distinguishable from "set to nothing".

jq and agent-box-envstore join contractPrograms; nothing else was needed.

The prose moves too

Every export carried a paragraph explaining itself — written once in Nix and once in Python. That is the same reasoning maintained in two places, which is how they drift in the first place. It now rides on the manifest entry and is emitted into both backends' wrappers.

Two divergences died on the way

Both were invisible to the box-wide comparison, because each name was supplied somewhere by both backends:

  • the module's agent-box-webhook wrapper never exported AGENT_BOX_HOOK_ARGS_OPTION_NAME; native's did. Now both do — and the WRAPPER_VARS_BY_DESIGN entry fix(native): the spawn wrapper never had the env its payload demands #500 added for it is deleted. A fixed gap must leave the table in the same change, and the staleness check enforces exactly that.
  • native's agent-box-webhook wrapper exported AGENT_BOX_HOOK_SESSION_ARGS, which nothing reads: webhook-cli.sh does not use it and never invokes the spawn payload — it delegates to webhook.py. The module never had it there. Now neither does, and the apostrophe test asserts the CLI wrapper does not carry it rather than looping over both wrappers looking for it.

The per-wrapper parity section is now empty of divergences rather than declaring one, which is the result to look for: the check is still there, but it has nothing left to report on these two.

Scope

The two webhook wrappers, which is where the divergence actually bit. The remaining hand-written prologues (session, profile, password, attach) are a follow-up; #500's comparisons stay as the backstop until they migrate.

Verification

NIX_BUILD_RC=0 over all 26 aarch64 checks, 85 native tests OK, PARITY_RC=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rt3Sp5XwGZy1Uzto8qP4is

…lists

#500 added a fourth parity comparison because a wrapper prologue could
diverge. This removes the ability to diverge: neither backend writes one any
more. modules/src/contract/wrappers.json declares each generated wrapper's
environment once — the export, and the reason it exists — and both renderers
render that.

The contract mechanism is issue #451's, one level over. It already carries a
UNIT's bindings as data that the module and bin/agentbox both read; a wrapper
belongs to no unit, which is exactly why it fell through: the webhook receiver
and the settings daemon both run agent-box-webhook-spawn, so what its payload
gets is the block above the `exec`. Two entry kinds:

  bin      names a PROGRAM, resolved by each backend the way it already
           resolves a unit contract's programs — contractPrograms here,
           self.bin/<program> natively. jq and agent-box-envstore join that
           table; nothing else was needed.
  config   names a per-box VALUE by key. The two backends may legitimately
           resolve one to different text, which is the point of naming a key:
           a diagnostic that says where the fleet-wide default LIVES has to
           name a NixOS option on one backend and config.yaml on the other.
           omitWhenEmpty drops the line when the value is empty, so "nothing
           set" stays distinguishable from "set to nothing".

The prose moves too. Every export carried a paragraph explaining itself, written
once in Nix and once in Python — the same reasoning maintained twice, which is
how they drift. It now rides on the manifest entry and is emitted into both
wrappers.

Two divergences died on the way, both invisible to the box-wide comparison
because each name was supplied SOMEWHERE by both backends:

  - the module's agent-box-webhook wrapper never exported
    AGENT_BOX_HOOK_ARGS_OPTION_NAME, native's did. Now both do, and the
    WRAPPER_VARS_BY_DESIGN entry #500 added for it is deleted — a fixed gap
    must leave the table in the same change, and the staleness check enforces
    that.
  - native's agent-box-webhook wrapper exported AGENT_BOX_HOOK_SESSION_ARGS,
    which nothing reads: webhook-cli.sh does not use it and never invokes the
    spawn payload, it delegates to webhook.py. The module never had it there.
    Now neither does, and the apostrophe test asserts the CLI wrapper does NOT
    carry it rather than looping over both wrappers looking for it.

Scope: the two webhook wrappers, which is where the divergence actually bit.
The remaining hand-written prologues (session, profile, password, attach) are a
follow-up; #500's comparisons stay as the backstop until they migrate, and the
per-wrapper section is now empty of divergences rather than declaring one.

NIX_BUILD_RC=0 over all 26 aarch64 checks, 85 native tests OK, PARITY_RC=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt3Sp5XwGZy1Uzto8qP4is
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 8fd4d93d-8329-4080-b584-eba6123cf251

📥 Commits

Reviewing files that changed from the base of the PR and between 15a9119 and 43e5e09.

📒 Files selected for processing (10)
  • bin/agentbox
  • modules/agent-box.nix
  • modules/agent-box.nix.in
  • modules/src/contract/wrappers.json
  • scripts/check_backend_parity.py
  • tests/golden/web/payloads/agent-box-webhook-spawn/bin/agent-box-webhook-spawn
  • tests/golden/web/payloads/agent-box-webhook/bin/agent-box-webhook
  • tests/native/expected/etc/agent-box/bin/agent-box-webhook-spawn
  • tests/native/expected/usr/local/bin/agent-box-webhook
  • tests/test_agentbox.py

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Webhook wrappers now use a shared manifest to generate environment exports. The renderer resolves binaries and configuration values, shell-quotes values, omits empty optional values, and keeps native and Nix-generated wrappers aligned.

Changes

Wrapper contract generation

Layer / File(s) Summary
Wrapper contract manifest
modules/src/contract/wrappers.json, modules/agent-box.nix, modules/agent-box.nix.in
Defines environment bindings for webhook and webhook-spawn wrappers, including binaries, configuration keys, rationale comments, and optional hook-session arguments.
Contract resolution and rendering
bin/agentbox, modules/agent-box.nix, modules/agent-box.nix.in
Adds contract lookup and wrapper rendering. Configuration values are resolved and shell-escaped. Binary paths use the program registry. Empty optional values are omitted.
Webhook integration and validation
bin/agentbox, modules/agent-box.nix, modules/agent-box.nix.in, scripts/check_backend_parity.py, tests/golden/..., tests/native/expected/..., tests/test_agentbox.py
Both webhook wrappers use generated prologues. Parity metadata, golden outputs, comments, and tests reflect spawn-only hook-session arguments.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 43e5e

The change centralizes webhook wrapper environment declarations across both backends, with the supplied builds, tests, and parity checks passing. No actionable merge-blocking risk remains.

Suggested reviewers: lionello

Sequence Diagram(s)

sequenceDiagram
  participant NixModule
  participant WrapperEnvRenderer
  participant ProgramRegistry
  participant BoxConfiguration
  participant WebhookWrapper
  NixModule->>WrapperEnvRenderer: render wrapper environment
  WrapperEnvRenderer->>ProgramRegistry: resolve pinned binaries
  WrapperEnvRenderer->>BoxConfiguration: resolve configured values
  WrapperEnvRenderer-->>WebhookWrapper: emit shell export prologue
  WebhookWrapper-->>WebhookWrapper: run with generated environment
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: moving wrapper prologues from duplicated hand-written lists into contract data.
Description check ✅ Passed The description directly explains the manifest-driven wrapper refactor, schema, backend changes, scope, and verification results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (8 skipped: 8 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (8 skipped: 8 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wrapper-prologue-contract

Comment @coderabbitai help to get the list of available commands.

@defangdevs
defangdevs merged commit 81cf0d2 into master Sep 2, 2026
3 checks passed
@defangdevs
defangdevs deleted the feat/wrapper-prologue-contract branch September 2, 2026 04:10
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Agent-Box Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant