feat(delivery): TOOL_INSTALLATION — first delivery type end to end (PR 2 of the plan) - #2212
Merged
Merged
Conversation
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
4 times, most recently
from
September 17, 2026 09:20
6f7bb7f to
369f246
Compare
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
5 times, most recently
from
September 21, 2026 10:30
2fdf810 to
480c0c0
Compare
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
15 times, most recently
from
September 22, 2026 11:34
612582d to
b2e4d9e
Compare
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
2 times, most recently
from
September 22, 2026 13:40
0acedc4 to
7358d93
Compare
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
from
September 22, 2026 15:18
7901846 to
4199655
Compare
…g and agent-version gate Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnYUpYgKvuMw6hB5VZSimY
semen-flamingo
force-pushed
the
feature/delivery-usage
branch
from
September 22, 2026 15:32
4199655 to
92a1345
Compare
semen-flamingo
marked this pull request as ready for review
September 22, 2026 20:28
Contributor
🦩 Flamingo Code Review7 finding(s) — 2 action required · 5 recommended · 0 informational Mode: advisory · Rules cited: Inline comments: 6 new Findings without an inline anchor in this diff
Need another pass? Commits pushed after this review are not reviewed automatically.
Prefer typing? Comment React 👍/👎 on inline comments to teach the reviewer. Started 2026-09-22 20:29 UTC · updated 2026-09-22 20:30 UTC · workflow run |
kirill-567
reviewed
Sep 22, 2026
kirill-567
reviewed
Sep 22, 2026
kirill-567
reviewed
Sep 22, 2026
kirill-567
reviewed
Sep 22, 2026
kirill-567
reviewed
Sep 22, 2026
…sion gate Review follow-up (#2212): - machine.{id}.delivery.result carries ACKED/DONE/FAILED with dispatchId for every type; InstalledAgentService and ScriptExecutionAcknowledgeListener are back to main - offline rows are postponed to the next sweep tick instead of parked and woken by a device event - per-type flag only; the agent-version gate and AgentVersion are gone - ToolInstallationDeliverySeed is a top-level class Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnYUpYgKvuMw6hB5VZSimY
An incomplete, unknown or malformed delivery result is an agent contract violation: error log + openframe.delivery.result.rejected{reason} for alerting, then acked so it cannot poison the consumer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnYUpYgKvuMw6hB5VZSimY
… back
DeliveryRef {type, targetId, dispatchId} replaces the bare dispatchId in the command body; the result message carries the same block, so the agent reports without knowing types or targets.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnYUpYgKvuMw6hB5VZSimY
kirill-567
previously approved these changes
Sep 23, 2026
…he row with Found on the feature tenant: a reinstall reopened the row as PENDING but kept failure=EXHAUSTED and finishedAt from the previous dispatch; null fields are not in the $set, so they are unset explicitly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnaktPhqs3aeuuUHdXCAjX
semen-flamingo
enabled auto-merge (squash)
September 24, 2026 11:12
yevhenii-flamingo
approved these changes
Sep 24, 2026
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First delivery type on the engine merged in #2200: TOOL_INSTALLATION end to end, behind a per-type flag with a real rollback. Four commits on top of
main(the last three are review follow-ups), 42 files.The flag
openframe.delivery.enabled.TOOL_INSTALLATIONis checked at the call site,ToolInstallationService.publish. A type not listed in that map is off: there is no defaults-level switch, so nothing can turn every type on at once.maintoday — same class, samejs.publish, no row, no retries. Flipping the flag back off is the rollback for this type; nothing else has to move.publishon the same subject (the stream still stores it, so agents that still hold a durable keep receiving it); the sweep on client retries; the agent's results onmachine.{id}.delivery.resultclose the row.Rollout order: the agent release that reports delivery results goes to the fleet first, the flag flips after. An agent that predates the release never reports, so with the flag on it would be re-sent to three times through its durable and the row would end FAILED/EXHAUSTED — bounded, visible, and only possible for a machine that was offline through the whole agent rollout and gets a tool command in its first minute back online. No version gate for that (was in an earlier revision, dropped on review).
The global
openframe.delivery.enabledfrom #2200 is gone: one switch per type (enabled.<TYPE>, missing = off) andsweep.enabledfor the sweep on client. Every next type gets the same call-site switch with its old code kept as the off-path until that type's cleanup.What this PR adds
The spec.
ToolInstallationDeliverySpecinopenframe-data-nats(which now depends onopenframe-machine-delivery) with its seedToolInstallationDeliverySeed(machineId, toolAgent, tool, reinstall):request(seed)maps the payload with the two data-nats mappers and setstargetId = toolAgent.getKey(),publishsends onmachine.{id}.tool-installationwith corepublish. It builds the message itself; the duplication withToolInstallationNatsPublisher.buildMessageis deliberate and temporary — the old publisher stays byte-identical as the off-path and is deleted with this type's cleanup.The
deliveryblock. Everydispatch()mints a randomdispatchIdand stamps the command with one block the agent never has to interpret:DeliveryRefis that block;DeliveryPayloadis the one-property interface every payload class implements (ToolInstallationMessageincluded,@JsonInclude(NON_NULL)so the old path's bytes do not change). The dispatcher sets it, the recorder stores thedispatchIdon the row, the sweep re-sends the storedpayloadJson— so every re-send of the same dispatch carries the same block and the agent can dedupe on it (re-report, do not reinstall).One generic result endpoint for every type. The agent reports on
machine.{id}.delivery.result(DeliveryResultMessage, JetStream streamDELIVERY_RESULTadded to the stream initializer):{ "delivery": { "type": "TOOL_INSTALLATION", "targetId": "fleetmdm-agent", "dispatchId": "…" }, "result": "ACKED" } { "delivery": { "type": "TOOL_INSTALLATION", "targetId": "fleetmdm-agent", "dispatchId": "…" }, "result": "DONE" } { "delivery": { "type": "TOOL_INSTALLATION", "targetId": "fleetmdm-agent", "dispatchId": "…" }, "result": "FAILED", "error": "…" }The
deliveryblock is the command's block copied back verbatim; the agent knows nothing about types or targets.DeliveryResultListenerin client-core routes toDeliveryTracker.acknowledge / complete / fail. Every transition is a conditional update that matches the row's currentdispatchId: a late report for the previous dispatch of the same command (reinstall twice) cannot ack or close the new row, and a report without adispatchIddoes nothing.FAILEDcloses the row withfailure = AGENT_ERRORand the agent'serrortext, counts the metric and calls the spec'sonFailed. A report without thedeliveryblock, with a field missing in it, or with atype/resultthis server does not know (reads as null), is a contract violation by the agent: it is rejected with an error log and the counteropenframe.delivery.result.rejected{reason=incomplete|malformed}(alert candidate), then acked so it cannot poison the consumer; a malformed payload goes the same way; a storage error leaves the message unacked for redelivery. No business listener is touched:InstalledAgentServiceandScriptExecutionAcknowledgeListenerare as onmain.Offline machines without an event listener. A row whose machine is not online is postponed to the next sweep tick (
sweep.interval, capped at the reconnect window end) instead of being parked formax-retry-intervaland woken by a device-online event. A machine that comes back is picked up within one tick; theparkedflag,wakeand the{tenantId, machineId}index are gone.Engine changes riding along:
DeliveryRef/DeliveryPayload+dispatchIdon the row and inmarkAcked/markDone/markFailed;DeliveryClosermoved totrack(unconditional: the tracker closes agent-reported failures through it, the sweep still closes exhausted/offline/timeout ones);DeliveryProperties.isEnabled(type)over the per-typeenabledmap;Sweep.intervalbound as a property (same key the scheduler already reads); the recorder no longer checks a flag (dispatch is the new path); thesweeppackage is conditional onsweep.enabledalone.Agent contract (for the agent team)
machine.{machineId}.tool-installationwith a plain (core) subscription, no durable consumer.machine.{machineId}.delivery.resultright away: the command'sdeliveryblock copied as is plusresult: ACKED. A command without adeliveryblock (old path) is not reported.delivery.dispatchIdwas already processed: reportACKEDagain, do not reinstall.DONE; on failure:FAILEDwitherror.installed-agentkeeps being sent as today (installed_agents bookkeeping); it no longer closes deliveries.Tests
ToolInstallationDeliverySpecTest(request mapping, empty-string defaults, core publish),DeliveryResultListenerTest(ACKED/DONE/FAILED routing; missing block, missing dispatchId, unknown result, unknown type and malformed payload rejected and counted; storage error left unacked),DeliveryMetricsTest,DeliveryPropertiesTest(type not listed / listed off / listed on),DeliveryTrackerTest/DeliveryCloserTest(dispatchId on complete, agent-reported failure),DeliveryDispatcherTest/DeliveryRecorderTest(dispatchId minted and stored),DeliverySweepServiceTest(offline rows postponed to the next tick),CustomMachineDeliveryRepositoryImplTest(dispatchId criteria on ack and agent failure).ToolInstallationServicehas no unit test becauseopenframe-tool-agent-nats-installationhas no test infrastructure at all.Verification
mvn -pl openframe-data-mongo-sync,openframe-machine-delivery,openframe-data-nats,openframe-client-core,openframe-api-service-core,openframe-management-service-core,openframe-tool-agent-nats-installation -am test -Dtest='Delivery*Test,*DeliverySpecTest,CustomMachineDeliveryRepositoryImplTest,MachineOnlineStatusTest,DeliveryResultListenerTest'— 112 delivery-related tests, build green on JDK 21.Verified on a feature tenant (openframe-saas-tenant#3330, dev, 2026-09-24)
Real macOS agent of today's release (no
delivery.resultyet),enabled.TOOL_INSTALLATION: true:dispatchId, old publisher not calleddispatchId, attempts from 0deliveryblock, row not touchedfailure/finishedAtfrom the previous dispatch;upsertPendingnow unsetsackedAt,finishedAt,failure,errorRollout rule this confirms: the agent release that reports on
delivery.result(and dedupes bydelivery.dispatchId) goes to the fleet first, the flag flips after. An agent that predates it gets every retry of a reinstall as a fresh reinstall.Tenant side needed for the rollout (in openframe-saas-tenant#3330):
openframe-data-natsin the rootdependencyManagement— saas-lib declares it at saas-lib's own oss version one hop closer than the delivery path, and Maven's nearest-wins otherwise dropsopenframe-machine-deliveryfrom saas-api (NoClassDefFoundError DeliverySeed); NATS device permissionsmachine.*.tool-installation(subscribe) andmachine.*.delivery.result(publish).🤖 Generated with Claude Code
https://claude.ai/code/session_01BnYUpYgKvuMw6hB5VZSimY