From f792d208ef64b445da3e35435e71e78ca020c51f Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 12:03:21 +0300 Subject: [PATCH] Add agent dispatch intent contract --- contracts/work/lease.go | 13 +++++++++++ dev/ACTIVE_TASK.json | 4 ++-- .../DEV-000120-agent-dispatch-intent.json | 23 +++++++++++++++++++ modules/work/domain/lease.go | 7 ++++++ tests/agent_execution/dispatch_test.go | 19 +++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 dev/tasks/DEV-000120-agent-dispatch-intent.json create mode 100644 tests/agent_execution/dispatch_test.go diff --git a/contracts/work/lease.go b/contracts/work/lease.go index 0927819..e515e9c 100644 --- a/contracts/work/lease.go +++ b/contracts/work/lease.go @@ -48,3 +48,16 @@ type IndependentVerification struct { Verified bool Summary string } + +type AgentDispatchIntent struct { + DispatchID string + IdempotencyKey string + DelegationID DelegationID + RunID AgentRunID + ImplementationID string + ImplementationRevision int64 + SourceRevision string + WorktreeID string + VerificationPlanRef string + CreatedAtUnixMillis int64 +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 3dbe2f3..6a88a96 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000119-independent-verification-contract.json", - "task_id": "DEV-000119", + "task_file": "dev/tasks/DEV-000120-agent-dispatch-intent.json", + "task_id": "DEV-000120", "task_revision": 1 } diff --git a/dev/tasks/DEV-000120-agent-dispatch-intent.json b/dev/tasks/DEV-000120-agent-dispatch-intent.json new file mode 100644 index 0000000..b5cfebb --- /dev/null +++ b/dev/tasks/DEV-000120-agent-dispatch-intent.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000120", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "04dad79", + "risk_tier": "E", + "intent": "Add typed durable dispatch intent and idempotency contract for agent runs without implementing a dispatcher, queue or worker process.", + "requirement_ids": ["WP-301", "Durable dispatch", "Duplicate dispatch prevention"], + "allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/**", "modules/work/domain/**", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000120-agent-dispatch-intent.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "cmd/**", "runtime/**", "adapters/**", ".gitea/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["ADR-0017 delegation/run ownership", "ADR-0020 dispatch idempotency", "Existing Capability implementation identity"], "may_change": ["Work dispatch intent contract"]}, + "semantic_owner": "work-agent-execution", + "canonical_state": [], + "state_transitions": ["dispatch intent -> accepted once by idempotency key -> lease acquisition", "same idempotency key -> same dispatch identity"], + "side_effects_and_privileges": ["Pure validation; no queue, process start, persistence adapter, credential access or external mutation"], + "persistence_and_migrations": ["No schema or migration changes"], + "failure_retry_restart": ["Retry reuses intent identity only when the durable store confirms the same payload; changed payload requires a new key"], + "assumptions": ["Dispatcher persists intent atomically later", "Exact source/worktree is resolved before worker start", "Verification plan is part of dispatch provenance"], + "acceptance_scenarios": ["Complete dispatch validates", "Missing idempotency/source/worktree/verification plan rejects", "Payload identity is explicit", "Worker implementation is typed and not inferred from free text"], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "dispatch intent tests"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/modules/work/domain/lease.go b/modules/work/domain/lease.go index 1103a99..f8159df 100644 --- a/modules/work/domain/lease.go +++ b/modules/work/domain/lease.go @@ -39,3 +39,10 @@ func ValidateIndependentVerification(value work.IndependentVerification) error { } return nil } + +func ValidateDispatchIntent(value work.AgentDispatchIntent) error { + if value.DispatchID == "" || value.IdempotencyKey == "" || value.DelegationID == "" || value.RunID == "" || value.ImplementationID == "" || value.ImplementationRevision < 1 || value.SourceRevision == "" || value.WorktreeID == "" || value.VerificationPlanRef == "" || value.CreatedAtUnixMillis < 1 { + return ErrCallbackRejected + } + return nil +} diff --git a/tests/agent_execution/dispatch_test.go b/tests/agent_execution/dispatch_test.go new file mode 100644 index 0000000..7b61e77 --- /dev/null +++ b/tests/agent_execution/dispatch_test.go @@ -0,0 +1,19 @@ +package agent_execution_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/work" + workdomain "git.cin.su/m/Assistant/modules/work/domain" +) + +func TestDispatchIntentRequiresStableExecutionIdentity(t *testing.T) { + intent := work.AgentDispatchIntent{DispatchID: "dispatch-1", IdempotencyKey: "idem-1", DelegationID: "delegation-1", RunID: "run-1", ImplementationID: "agent-1", ImplementationRevision: 2, SourceRevision: "commit-1", WorktreeID: "worktree-1", VerificationPlanRef: "verify-plan-1", CreatedAtUnixMillis: 10} + if err := workdomain.ValidateDispatchIntent(intent); err != nil { + t.Fatal(err) + } + intent.IdempotencyKey = "" + if err := workdomain.ValidateDispatchIntent(intent); err == nil { + t.Fatal("expected missing idempotency rejection") + } +}