From 1860d6cca89f1bf69df789f48efcc55d23842489 Mon Sep 17 00:00:00 2001 From: pchmirenko Date: Tue, 14 Apr 2026 14:27:20 +0200 Subject: [PATCH] feat: gate task claims on job spec pointers --- artifacts/anchor/idl/agenc_coordination.json | 411 +++++++++++++++++- artifacts/anchor/manifest.json | 4 +- artifacts/anchor/types/agenc_coordination.ts | 409 +++++++++++++++++ package-lock.json | 6 +- .../src/generated/agenc_coordination.json | 411 +++++++++++++++++- .../src/generated/agenc_coordination.ts | 409 +++++++++++++++++ packages/protocol/src/generated/manifest.json | 4 +- programs/agenc-coordination/src/errors.rs | 9 + programs/agenc-coordination/src/events.rs | 10 + .../src/instructions/bid_marketplace.rs | 34 +- .../src/instructions/claim_task.rs | 162 ++++++- .../src/instructions/create_dependent_task.rs | 20 +- .../src/instructions/create_task.rs | 22 +- .../src/instructions/initiate_dispute.rs | 24 +- .../src/instructions/mod.rs | 3 + .../src/instructions/set_task_job_spec.rs | 134 ++++++ programs/agenc-coordination/src/lib.rs | 14 + programs/agenc-coordination/src/state.rs | 36 ++ 18 files changed, 2050 insertions(+), 72 deletions(-) create mode 100644 programs/agenc-coordination/src/instructions/set_task_job_spec.rs diff --git a/artifacts/anchor/idl/agenc_coordination.json b/artifacts/anchor/idl/agenc_coordination.json index 157bfed4..638092d8 100644 --- a/artifacts/anchor/idl/agenc_coordination.json +++ b/artifacts/anchor/idl/agenc_coordination.json @@ -6327,6 +6327,260 @@ ], "args": [] }, + { + "name": "set_task_job_spec", + "docs": [ + "Attach or update a content-addressed off-chain job specification pointer for a task." + ], + "discriminator": [ + 134, + 102, + 102, + 86, + 31, + 164, + 202, + 193 + ], + "accounts": [ + { + "name": "task", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "Task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "Task" + } + ] + } + }, + { + "name": "task_job_spec", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "job_spec_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "type": "string" + } + ] + }, + { + "name": "claim_task_with_job_spec", + "docs": [ + "Claim a task only when its content-addressed job specification pointer exists." + ], + "discriminator": [ + 230, + 40, + 107, + 109, + 208, + 228, + 175, + 31 + ], + "accounts": [ + { + "name": "task", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "Task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "Task" + } + ] + } + }, + { + "name": "task_job_spec", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "claim", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 108, + 97, + 105, + 109 + ] + }, + { + "kind": "account", + "path": "task" + }, + { + "kind": "account", + "path": "worker" + } + ] + } + }, + { + "name": "protocol_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 116, + 111, + 99, + 111, + 108 + ] + } + ] + } + }, + { + "name": "worker", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 101, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "worker.agent_id", + "account": "AgentRegistration" + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "worker" + ] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "stake_reputation", "docs": [ @@ -9040,6 +9294,19 @@ 187 ] }, + { + "name": "TaskJobSpec", + "discriminator": [ + 249, + 63, + 211, + 94, + 228, + 165, + 3, + 196 + ] + }, { "name": "TaskSubmission", "discriminator": [ @@ -9783,6 +10050,19 @@ 175 ] }, + { + "name": "TaskJobSpecSet", + "discriminator": [ + 155, + 2, + 153, + 121, + 179, + 90, + 208, + 183 + ] + }, { "name": "TaskResultAccepted", "discriminator": [ @@ -11078,6 +11358,21 @@ "code": 6237, "name": "RateLimitBelowMinimum", "msg": "Rate limit value below protocol minimum" + }, + { + "code": 6238, + "name": "InvalidTaskJobSpecHash", + "msg": "Invalid task job specification hash" + }, + { + "code": 6239, + "name": "InvalidTaskJobSpecUri", + "msg": "Invalid task job specification URI" + }, + { + "code": 6240, + "name": "TaskJobSpecTaskMismatch", + "msg": "Task job specification account does not belong to this task" } ], "types": [ @@ -15819,6 +16114,120 @@ ] } }, + { + "name": "TaskJobSpec", + "docs": [ + "Content-addressed pointer to a task's full off-chain job specification.", + "PDA seeds: [\"task_job_spec\", task]" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "docs": [ + "Task this job specification belongs to." + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "Task creator authorized to set or update the pointer." + ], + "type": "pubkey" + }, + { + "name": "job_spec_hash", + "docs": [ + "SHA-256 hash of the canonicalized job specification payload." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "docs": [ + "URI where the canonicalized job specification payload can be fetched." + ], + "type": "string" + }, + { + "name": "created_at", + "docs": [ + "Creation timestamp." + ], + "type": "i64" + }, + { + "name": "updated_at", + "docs": [ + "Last update timestamp." + ], + "type": "i64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "_reserved", + "docs": [ + "Reserved for future metadata flags." + ], + "type": { + "array": [ + "u8", + 7 + ] + } + } + ] + } + }, + { + "name": "TaskJobSpecSet", + "docs": [ + "Emitted when a task's full off-chain job specification pointer is set." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "job_spec_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "TaskResultAccepted", "docs": [ @@ -16562,4 +16971,4 @@ } } ] -} \ No newline at end of file +} diff --git a/artifacts/anchor/manifest.json b/artifacts/anchor/manifest.json index c8c4eb8d..6aa84b98 100644 --- a/artifacts/anchor/manifest.json +++ b/artifacts/anchor/manifest.json @@ -8,11 +8,11 @@ "artifacts": { "idl": { "path": "artifacts/anchor/idl/agenc_coordination.json", - "sha256": "d4476fc6ccee9d01a9bf77a242b6983b35c6cb76cbb63da5a78b19c026652f04" + "sha256": "a732c01c6b0d62a00eac9412f870dced2f3f30a8a9e159ef3f6b2a53fc812bd6" }, "types": { "path": "artifacts/anchor/types/agenc_coordination.ts", - "sha256": "e98719e09e553b1457a4dff77d7c691eabd680db42ecef86ce776b436e0a9040" + "sha256": "24a0460df83629d98f06a09e426943ccb96ac024561ceb026012e1260c71979f" }, "verifierRouterIdl": { "path": "scripts/idl/verifier_router.json" diff --git a/artifacts/anchor/types/agenc_coordination.ts b/artifacts/anchor/types/agenc_coordination.ts index 9ea10123..2acd091a 100644 --- a/artifacts/anchor/types/agenc_coordination.ts +++ b/artifacts/anchor/types/agenc_coordination.ts @@ -1566,6 +1566,162 @@ export type AgencCoordination = { ], "args": [] }, + { + "name": "claimTaskWithJobSpec", + "docs": [ + "Claim a task only when its content-addressed job specification pointer exists." + ], + "discriminator": [ + 230, + 40, + 107, + 109, + 208, + 228, + 175, + 31 + ], + "accounts": [ + { + "name": "task", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "task" + } + ] + } + }, + { + "name": "taskJobSpec", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "claim", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 108, + 97, + 105, + 109 + ] + }, + { + "kind": "account", + "path": "task" + }, + { + "kind": "account", + "path": "worker" + } + ] + } + }, + { + "name": "protocolConfig", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 116, + 111, + 99, + 111, + 108 + ] + } + ] + } + }, + { + "name": "worker", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 101, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "worker.agent_id", + "account": "agentRegistration" + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "worker" + ] + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "completeTask", "docs": [ @@ -6333,6 +6489,104 @@ export type AgencCoordination = { ], "args": [] }, + { + "name": "setTaskJobSpec", + "docs": [ + "Attach or update a content-addressed off-chain job specification pointer for a task." + ], + "discriminator": [ + 134, + 102, + 102, + 86, + 31, + 164, + 202, + 193 + ], + "accounts": [ + { + "name": "task", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "task" + } + ] + } + }, + { + "name": "taskJobSpec", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "jobSpecHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "type": "string" + } + ] + }, { "name": "stakeReputation", "docs": [ @@ -9046,6 +9300,19 @@ export type AgencCoordination = { 187 ] }, + { + "name": "taskJobSpec", + "discriminator": [ + 249, + 63, + 211, + 94, + 228, + 165, + 3, + 196 + ] + }, { "name": "taskSubmission", "discriminator": [ @@ -9789,6 +10056,19 @@ export type AgencCoordination = { 175 ] }, + { + "name": "taskJobSpecSet", + "discriminator": [ + 155, + 2, + 153, + 121, + 179, + 90, + 208, + 183 + ] + }, { "name": "taskResultAccepted", "discriminator": [ @@ -11084,6 +11364,21 @@ export type AgencCoordination = { "code": 6237, "name": "rateLimitBelowMinimum", "msg": "Rate limit value below protocol minimum" + }, + { + "code": 6238, + "name": "invalidTaskJobSpecHash", + "msg": "Invalid task job specification hash" + }, + { + "code": 6239, + "name": "invalidTaskJobSpecUri", + "msg": "Invalid task job specification URI" + }, + { + "code": 6240, + "name": "taskJobSpecTaskMismatch", + "msg": "Task job specification account does not belong to this task" } ], "types": [ @@ -15825,6 +16120,120 @@ export type AgencCoordination = { ] } }, + { + "name": "taskJobSpec", + "docs": [ + "Content-addressed pointer to a task's full off-chain job specification.", + "PDA seeds: [\"task_job_spec\", task]" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "docs": [ + "Task this job specification belongs to." + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "Task creator authorized to set or update the pointer." + ], + "type": "pubkey" + }, + { + "name": "jobSpecHash", + "docs": [ + "SHA-256 hash of the canonicalized job specification payload." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "docs": [ + "URI where the canonicalized job specification payload can be fetched." + ], + "type": "string" + }, + { + "name": "createdAt", + "docs": [ + "Creation timestamp." + ], + "type": "i64" + }, + { + "name": "updatedAt", + "docs": [ + "Last update timestamp." + ], + "type": "i64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future metadata flags." + ], + "type": { + "array": [ + "u8", + 7 + ] + } + } + ] + } + }, + { + "name": "taskJobSpecSet", + "docs": [ + "Emitted when a task's full off-chain job specification pointer is set." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "jobSpecHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "taskResultAccepted", "docs": [ diff --git a/package-lock.json b/package-lock.json index b03b03ef..23a4f506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1169,9 +1169,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/packages/protocol/src/generated/agenc_coordination.json b/packages/protocol/src/generated/agenc_coordination.json index 157bfed4..638092d8 100644 --- a/packages/protocol/src/generated/agenc_coordination.json +++ b/packages/protocol/src/generated/agenc_coordination.json @@ -6327,6 +6327,260 @@ ], "args": [] }, + { + "name": "set_task_job_spec", + "docs": [ + "Attach or update a content-addressed off-chain job specification pointer for a task." + ], + "discriminator": [ + 134, + 102, + 102, + 86, + 31, + 164, + 202, + 193 + ], + "accounts": [ + { + "name": "task", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "Task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "Task" + } + ] + } + }, + { + "name": "task_job_spec", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "job_spec_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "type": "string" + } + ] + }, + { + "name": "claim_task_with_job_spec", + "docs": [ + "Claim a task only when its content-addressed job specification pointer exists." + ], + "discriminator": [ + 230, + 40, + 107, + 109, + 208, + 228, + 175, + 31 + ], + "accounts": [ + { + "name": "task", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "Task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "Task" + } + ] + } + }, + { + "name": "task_job_spec", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "claim", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 108, + 97, + 105, + 109 + ] + }, + { + "kind": "account", + "path": "task" + }, + { + "kind": "account", + "path": "worker" + } + ] + } + }, + { + "name": "protocol_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 116, + 111, + 99, + 111, + 108 + ] + } + ] + } + }, + { + "name": "worker", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 101, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "worker.agent_id", + "account": "AgentRegistration" + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "worker" + ] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "stake_reputation", "docs": [ @@ -9040,6 +9294,19 @@ 187 ] }, + { + "name": "TaskJobSpec", + "discriminator": [ + 249, + 63, + 211, + 94, + 228, + 165, + 3, + 196 + ] + }, { "name": "TaskSubmission", "discriminator": [ @@ -9783,6 +10050,19 @@ 175 ] }, + { + "name": "TaskJobSpecSet", + "discriminator": [ + 155, + 2, + 153, + 121, + 179, + 90, + 208, + 183 + ] + }, { "name": "TaskResultAccepted", "discriminator": [ @@ -11078,6 +11358,21 @@ "code": 6237, "name": "RateLimitBelowMinimum", "msg": "Rate limit value below protocol minimum" + }, + { + "code": 6238, + "name": "InvalidTaskJobSpecHash", + "msg": "Invalid task job specification hash" + }, + { + "code": 6239, + "name": "InvalidTaskJobSpecUri", + "msg": "Invalid task job specification URI" + }, + { + "code": 6240, + "name": "TaskJobSpecTaskMismatch", + "msg": "Task job specification account does not belong to this task" } ], "types": [ @@ -15819,6 +16114,120 @@ ] } }, + { + "name": "TaskJobSpec", + "docs": [ + "Content-addressed pointer to a task's full off-chain job specification.", + "PDA seeds: [\"task_job_spec\", task]" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "docs": [ + "Task this job specification belongs to." + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "Task creator authorized to set or update the pointer." + ], + "type": "pubkey" + }, + { + "name": "job_spec_hash", + "docs": [ + "SHA-256 hash of the canonicalized job specification payload." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "docs": [ + "URI where the canonicalized job specification payload can be fetched." + ], + "type": "string" + }, + { + "name": "created_at", + "docs": [ + "Creation timestamp." + ], + "type": "i64" + }, + { + "name": "updated_at", + "docs": [ + "Last update timestamp." + ], + "type": "i64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "_reserved", + "docs": [ + "Reserved for future metadata flags." + ], + "type": { + "array": [ + "u8", + 7 + ] + } + } + ] + } + }, + { + "name": "TaskJobSpecSet", + "docs": [ + "Emitted when a task's full off-chain job specification pointer is set." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "job_spec_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "job_spec_uri", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "TaskResultAccepted", "docs": [ @@ -16562,4 +16971,4 @@ } } ] -} \ No newline at end of file +} diff --git a/packages/protocol/src/generated/agenc_coordination.ts b/packages/protocol/src/generated/agenc_coordination.ts index 9887df85..3274a137 100644 --- a/packages/protocol/src/generated/agenc_coordination.ts +++ b/packages/protocol/src/generated/agenc_coordination.ts @@ -1566,6 +1566,162 @@ export type AgencCoordination = { ], "args": [] }, + { + "name": "claimTaskWithJobSpec", + "docs": [ + "Claim a task only when its content-addressed job specification pointer exists." + ], + "discriminator": [ + 230, + 40, + 107, + 109, + 208, + 228, + 175, + 31 + ], + "accounts": [ + { + "name": "task", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "task" + } + ] + } + }, + { + "name": "taskJobSpec", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "claim", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 108, + 97, + 105, + 109 + ] + }, + { + "kind": "account", + "path": "task" + }, + { + "kind": "account", + "path": "worker" + } + ] + } + }, + { + "name": "protocolConfig", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 116, + 111, + 99, + 111, + 108 + ] + } + ] + } + }, + { + "name": "worker", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 101, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "worker.agent_id", + "account": "agentRegistration" + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "worker" + ] + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, { "name": "completeTask", "docs": [ @@ -6333,6 +6489,104 @@ export type AgencCoordination = { ], "args": [] }, + { + "name": "setTaskJobSpec", + "docs": [ + "Attach or update a content-addressed off-chain job specification pointer for a task." + ], + "discriminator": [ + 134, + 102, + 102, + 86, + 31, + 164, + 202, + 193 + ], + "accounts": [ + { + "name": "task", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107 + ] + }, + { + "kind": "account", + "path": "task.creator", + "account": "task" + }, + { + "kind": "account", + "path": "task.task_id", + "account": "task" + } + ] + } + }, + { + "name": "taskJobSpec", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 97, + 115, + 107, + 95, + 106, + 111, + 98, + 95, + 115, + 112, + 101, + 99 + ] + }, + { + "kind": "account", + "path": "task" + } + ] + } + }, + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "jobSpecHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "type": "string" + } + ] + }, { "name": "stakeReputation", "docs": [ @@ -9046,6 +9300,19 @@ export type AgencCoordination = { 187 ] }, + { + "name": "taskJobSpec", + "discriminator": [ + 249, + 63, + 211, + 94, + 228, + 165, + 3, + 196 + ] + }, { "name": "taskSubmission", "discriminator": [ @@ -9789,6 +10056,19 @@ export type AgencCoordination = { 175 ] }, + { + "name": "taskJobSpecSet", + "discriminator": [ + 155, + 2, + 153, + 121, + 179, + 90, + 208, + 183 + ] + }, { "name": "taskResultAccepted", "discriminator": [ @@ -11084,6 +11364,21 @@ export type AgencCoordination = { "code": 6237, "name": "rateLimitBelowMinimum", "msg": "Rate limit value below protocol minimum" + }, + { + "code": 6238, + "name": "invalidTaskJobSpecHash", + "msg": "Invalid task job specification hash" + }, + { + "code": 6239, + "name": "invalidTaskJobSpecUri", + "msg": "Invalid task job specification URI" + }, + { + "code": 6240, + "name": "taskJobSpecTaskMismatch", + "msg": "Task job specification account does not belong to this task" } ], "types": [ @@ -15825,6 +16120,120 @@ export type AgencCoordination = { ] } }, + { + "name": "taskJobSpec", + "docs": [ + "Content-addressed pointer to a task's full off-chain job specification.", + "PDA seeds: [\"task_job_spec\", task]" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "docs": [ + "Task this job specification belongs to." + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "Task creator authorized to set or update the pointer." + ], + "type": "pubkey" + }, + { + "name": "jobSpecHash", + "docs": [ + "SHA-256 hash of the canonicalized job specification payload." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "docs": [ + "URI where the canonicalized job specification payload can be fetched." + ], + "type": "string" + }, + { + "name": "createdAt", + "docs": [ + "Creation timestamp." + ], + "type": "i64" + }, + { + "name": "updatedAt", + "docs": [ + "Last update timestamp." + ], + "type": "i64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future metadata flags." + ], + "type": { + "array": [ + "u8", + 7 + ] + } + } + ] + } + }, + { + "name": "taskJobSpecSet", + "docs": [ + "Emitted when a task's full off-chain job specification pointer is set." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "task", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "jobSpecHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "jobSpecUri", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "taskResultAccepted", "docs": [ diff --git a/packages/protocol/src/generated/manifest.json b/packages/protocol/src/generated/manifest.json index c8c4eb8d..6aa84b98 100644 --- a/packages/protocol/src/generated/manifest.json +++ b/packages/protocol/src/generated/manifest.json @@ -8,11 +8,11 @@ "artifacts": { "idl": { "path": "artifacts/anchor/idl/agenc_coordination.json", - "sha256": "d4476fc6ccee9d01a9bf77a242b6983b35c6cb76cbb63da5a78b19c026652f04" + "sha256": "a732c01c6b0d62a00eac9412f870dced2f3f30a8a9e159ef3f6b2a53fc812bd6" }, "types": { "path": "artifacts/anchor/types/agenc_coordination.ts", - "sha256": "e98719e09e553b1457a4dff77d7c691eabd680db42ecef86ce776b436e0a9040" + "sha256": "24a0460df83629d98f06a09e426943ccb96ac024561ceb026012e1260c71979f" }, "verifierRouterIdl": { "path": "scripts/idl/verifier_router.json" diff --git a/programs/agenc-coordination/src/errors.rs b/programs/agenc-coordination/src/errors.rs index 8c74757e..ec74151c 100644 --- a/programs/agenc-coordination/src/errors.rs +++ b/programs/agenc-coordination/src/errors.rs @@ -750,4 +750,13 @@ pub enum CoordinationError { #[msg("Rate limit value below protocol minimum")] RateLimitBelowMinimum, + + #[msg("Invalid task job specification hash")] + InvalidTaskJobSpecHash, + + #[msg("Invalid task job specification URI")] + InvalidTaskJobSpecUri, + + #[msg("Task job specification account does not belong to this task")] + TaskJobSpecTaskMismatch, } diff --git a/programs/agenc-coordination/src/events.rs b/programs/agenc-coordination/src/events.rs index b435ed72..61c7aeb2 100644 --- a/programs/agenc-coordination/src/events.rs +++ b/programs/agenc-coordination/src/events.rs @@ -80,6 +80,16 @@ pub struct TaskCreated { pub timestamp: i64, } +/// Emitted when a task's full off-chain job specification pointer is set. +#[event] +pub struct TaskJobSpecSet { + pub task: Pubkey, + pub creator: Pubkey, + pub job_spec_hash: [u8; HASH_SIZE], + pub job_spec_uri: String, + pub timestamp: i64, +} + /// Emitted when a task with dependencies is created #[event] pub struct DependentTaskCreated { diff --git a/programs/agenc-coordination/src/instructions/bid_marketplace.rs b/programs/agenc-coordination/src/instructions/bid_marketplace.rs index a1f10fcb..a326371a 100644 --- a/programs/agenc-coordination/src/instructions/bid_marketplace.rs +++ b/programs/agenc-coordination/src/instructions/bid_marketplace.rs @@ -269,7 +269,7 @@ pub struct InitializeBidBook<'info> { bump = task.bump, constraint = task.creator == creator.key() @ CoordinationError::UnauthorizedTaskAction )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( init, @@ -278,7 +278,7 @@ pub struct InitializeBidBook<'info> { seeds = [b"bid_book", task.key().as_ref()], bump )] - pub bid_book: Account<'info, TaskBidBook>, + pub bid_book: Box>, #[account( seeds = [b"protocol"], @@ -595,7 +595,7 @@ pub struct UpdateBid<'info> { seeds = [b"task", task.creator.as_ref(), task.task_id.as_ref()], bump = task.bump )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( mut, @@ -603,7 +603,7 @@ pub struct UpdateBid<'info> { bump = bid_book.bump, constraint = bid_book.task == task.key() @ CoordinationError::InvalidInput )] - pub bid_book: Account<'info, TaskBidBook>, + pub bid_book: Box>, #[account( mut, @@ -612,7 +612,7 @@ pub struct UpdateBid<'info> { constraint = bid.task == task.key() @ CoordinationError::InvalidInput, constraint = bid.bid_book == bid_book.key() @ CoordinationError::InvalidInput )] - pub bid: Account<'info, TaskBid>, + pub bid: Box>, #[account( mut, @@ -620,7 +620,7 @@ pub struct UpdateBid<'info> { bump = bidder.bump, has_one = authority @ CoordinationError::UnauthorizedAgent )] - pub bidder: Account<'info, AgentRegistration>, + pub bidder: Box>, pub authority: Signer<'info>, @@ -628,13 +628,13 @@ pub struct UpdateBid<'info> { seeds = [b"bid_marketplace"], bump = bid_marketplace.bump )] - pub bid_marketplace: Account<'info, BidMarketplaceConfig>, + pub bid_marketplace: Box>, #[account( seeds = [b"protocol"], bump = protocol_config.bump )] - pub protocol_config: Account<'info, ProtocolConfig>, + pub protocol_config: Box>, } #[allow(clippy::too_many_arguments)] @@ -692,7 +692,7 @@ pub fn update_bid_handler( let bidder_key = ctx.accounts.bidder.key(); let bid_book_key = ctx.accounts.bid_book.key(); - let bid = &mut ctx.accounts.bid; + let bid = ctx.accounts.bid.as_mut(); bid.requested_reward_lamports = requested_reward_lamports; bid.eta_seconds = eta_seconds; bid.confidence_bps = confidence_bps; @@ -702,7 +702,7 @@ pub fn update_bid_handler( bid.expires_at = expires_at; bid.updated_at = now; - let bid_book = &mut ctx.accounts.bid_book; + let bid_book = ctx.accounts.bid_book.as_mut(); bid_book.version = bid_book .version .checked_add(1) @@ -730,7 +730,7 @@ pub struct CancelBid<'info> { seeds = [b"task", task.creator.as_ref(), task.task_id.as_ref()], bump = task.bump )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( mut, @@ -738,7 +738,7 @@ pub struct CancelBid<'info> { bump = bid_book.bump, constraint = bid_book.task == task.key() @ CoordinationError::InvalidInput )] - pub bid_book: Account<'info, TaskBidBook>, + pub bid_book: Box>, #[account( mut, @@ -748,7 +748,7 @@ pub struct CancelBid<'info> { constraint = bid.task == task.key() @ CoordinationError::InvalidInput, constraint = bid.bidder_authority == authority.key() @ CoordinationError::UnauthorizedAgent )] - pub bid: Account<'info, TaskBid>, + pub bid: Box>, #[account( mut, @@ -756,13 +756,13 @@ pub struct CancelBid<'info> { bump = bidder_market_state.bump, constraint = bidder_market_state.bidder == bidder.key() @ CoordinationError::InvalidInput )] - pub bidder_market_state: Account<'info, BidderMarketState>, + pub bidder_market_state: Box>, #[account( seeds = [b"agent", bidder.agent_id.as_ref()], bump = bidder.bump )] - pub bidder: Account<'info, AgentRegistration>, + pub bidder: Box>, #[account(mut)] pub authority: Signer<'info>, @@ -791,7 +791,7 @@ pub fn cancel_bid_handler(ctx: Context) -> Result<()> { let bid_key = ctx.accounts.bid.key(); let bidder_key = ctx.accounts.bidder.key(); let bid_book_key = ctx.accounts.bid_book.key(); - let bid_book = &mut ctx.accounts.bid_book; + let bid_book = ctx.accounts.bid_book.as_mut(); bid_book.active_bids = bid_book .active_bids .checked_sub(1) @@ -802,7 +802,7 @@ pub fn cancel_bid_handler(ctx: Context) -> Result<()> { .ok_or(CoordinationError::ArithmeticOverflow)?; bid_book.updated_at = now; - let bidder_state = &mut ctx.accounts.bidder_market_state; + let bidder_state = ctx.accounts.bidder_market_state.as_mut(); bidder_state.active_bid_count = bidder_state .active_bid_count .checked_sub(1) diff --git a/programs/agenc-coordination/src/instructions/claim_task.rs b/programs/agenc-coordination/src/instructions/claim_task.rs index ed627a7d..45613b5a 100644 --- a/programs/agenc-coordination/src/instructions/claim_task.rs +++ b/programs/agenc-coordination/src/instructions/claim_task.rs @@ -7,7 +7,8 @@ use crate::instructions::constants::{ }; use crate::instructions::task_validation_helpers::is_manual_validation_task; use crate::state::{ - AgentRegistration, AgentStatus, ProtocolConfig, Task, TaskClaim, TaskStatus, TaskType, + AgentRegistration, AgentStatus, ProtocolConfig, Task, TaskClaim, TaskJobSpec, TaskStatus, + TaskType, }; use crate::utils::version::check_version_compatible; use anchor_lang::prelude::*; @@ -19,7 +20,7 @@ pub struct ClaimTask<'info> { seeds = [b"task", task.creator.as_ref(), task.task_id.as_ref()], bump = task.bump )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( init_if_needed, @@ -29,13 +30,13 @@ pub struct ClaimTask<'info> { bump, constraint = claim.key() != task.key() @ CoordinationError::InvalidInput )] - pub claim: Account<'info, TaskClaim>, + pub claim: Box>, #[account( seeds = [b"protocol"], bump = protocol_config.bump )] - pub protocol_config: Account<'info, ProtocolConfig>, + pub protocol_config: Box>, #[account( mut, @@ -43,7 +44,54 @@ pub struct ClaimTask<'info> { bump = worker.bump, has_one = authority @ CoordinationError::UnauthorizedAgent )] - pub worker: Account<'info, AgentRegistration>, + pub worker: Box>, + + #[account(mut)] + pub authority: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct ClaimTaskWithJobSpec<'info> { + #[account( + mut, + seeds = [b"task", task.creator.as_ref(), task.task_id.as_ref()], + bump = task.bump + )] + pub task: Box>, + + #[account( + seeds = [b"task_job_spec", task.key().as_ref()], + bump = task_job_spec.bump, + constraint = task_job_spec.task == task.key() @ CoordinationError::TaskJobSpecTaskMismatch, + constraint = task_job_spec.creator == task.creator @ CoordinationError::UnauthorizedTaskAction + )] + pub task_job_spec: Box>, + + #[account( + init_if_needed, + payer = authority, + space = TaskClaim::SIZE, + seeds = [b"claim", task.key().as_ref(), worker.key().as_ref()], + bump, + constraint = claim.key() != task.key() @ CoordinationError::InvalidInput + )] + pub claim: Box>, + + #[account( + seeds = [b"protocol"], + bump = protocol_config.bump + )] + pub protocol_config: Box>, + + #[account( + mut, + seeds = [b"agent", worker.agent_id.as_ref()], + bump = worker.bump, + has_one = authority @ CoordinationError::UnauthorizedAgent + )] + pub worker: Box>, #[account(mut)] pub authority: Signer<'info>, @@ -52,10 +100,98 @@ pub struct ClaimTask<'info> { } pub fn handler(ctx: Context) -> Result<()> { - let task = &mut ctx.accounts.task; - let worker = &mut ctx.accounts.worker; - let claim = &mut ctx.accounts.claim; - let config = &ctx.accounts.protocol_config; + let task_key = ctx.accounts.task.key(); + let worker_key = ctx.accounts.worker.key(); + + process_claim( + task_key, + ctx.accounts.task.as_mut(), + ctx.accounts.claim.as_mut(), + ctx.accounts.protocol_config.as_ref(), + worker_key, + ctx.accounts.worker.as_mut(), + ctx.bumps.claim, + ) +} + +pub fn handler_with_job_spec(ctx: Context) -> Result<()> { + validate_job_spec_pointer(ctx.accounts.task_job_spec.as_ref())?; + + let task_key = ctx.accounts.task.key(); + let worker_key = ctx.accounts.worker.key(); + + process_claim( + task_key, + ctx.accounts.task.as_mut(), + ctx.accounts.claim.as_mut(), + ctx.accounts.protocol_config.as_ref(), + worker_key, + ctx.accounts.worker.as_mut(), + ctx.bumps.claim, + ) +} + +fn validate_job_spec_pointer(task_job_spec: &TaskJobSpec) -> Result<()> { + require!( + task_job_spec.job_spec_hash.iter().any(|byte| *byte != 0), + CoordinationError::InvalidTaskJobSpecHash + ); + require!( + !task_job_spec.job_spec_uri.trim().is_empty(), + CoordinationError::InvalidTaskJobSpecUri + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn task_job_spec(job_spec_hash: [u8; 32], job_spec_uri: &str) -> TaskJobSpec { + TaskJobSpec { + task: Pubkey::new_unique(), + creator: Pubkey::new_unique(), + job_spec_hash, + job_spec_uri: job_spec_uri.to_string(), + created_at: 1, + updated_at: 1, + bump: 255, + _reserved: [0; 7], + } + } + + #[test] + fn validates_non_empty_job_spec_pointer() { + let pointer = task_job_spec([1; 32], "agenc://job-spec/sha256/test"); + + assert!(validate_job_spec_pointer(&pointer).is_ok()); + } + + #[test] + fn rejects_zero_hash_job_spec_pointer() { + let pointer = task_job_spec([0; 32], "agenc://job-spec/sha256/test"); + + assert!(validate_job_spec_pointer(&pointer).is_err()); + } + + #[test] + fn rejects_blank_uri_job_spec_pointer() { + let pointer = task_job_spec([1; 32], " "); + + assert!(validate_job_spec_pointer(&pointer).is_err()); + } +} + +fn process_claim( + task_key: Pubkey, + task: &mut Account, + claim: &mut Account, + config: &Account, + worker_key: Pubkey, + worker: &mut Account, + claim_bump: u8, +) -> Result<()> { let clock = Clock::get()?; check_version_compatible(config)?; @@ -190,8 +326,8 @@ pub fn handler(ctx: Context) -> Result<()> { }; // Initialize claim - claim.task = task.key(); - claim.worker = worker.key(); + claim.task = task_key; + claim.worker = worker_key; claim.claimed_at = clock.unix_timestamp; claim.expires_at = expires_at; claim.completed_at = 0; @@ -200,7 +336,7 @@ pub fn handler(ctx: Context) -> Result<()> { claim.is_completed = false; claim.is_validated = false; claim.reward_paid = 0; - claim.bump = ctx.bumps.claim; + claim.bump = claim_bump; // Update task task.current_workers = task @@ -220,7 +356,7 @@ pub fn handler(ctx: Context) -> Result<()> { emit!(TaskClaimed { task_id: task.task_id, - worker: worker.key(), + worker: worker_key, current_workers: task.current_workers, max_workers: task.max_workers, timestamp: clock.unix_timestamp, diff --git a/programs/agenc-coordination/src/instructions/create_dependent_task.rs b/programs/agenc-coordination/src/instructions/create_dependent_task.rs index 97c32f2a..65719c57 100644 --- a/programs/agenc-coordination/src/instructions/create_dependent_task.rs +++ b/programs/agenc-coordination/src/instructions/create_dependent_task.rs @@ -29,7 +29,7 @@ pub struct CreateDependentTask<'info> { seeds = [b"task", creator.key().as_ref(), task_id.as_ref()], bump )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( init, @@ -38,7 +38,7 @@ pub struct CreateDependentTask<'info> { seeds = [b"escrow", task.key().as_ref()], bump )] - pub escrow: Account<'info, TaskEscrow>, + pub escrow: Box>, /// The parent task this new task depends on /// Note: Uses Box to reduce stack usage for this large account @@ -62,7 +62,7 @@ pub struct CreateDependentTask<'info> { bump = creator_agent.bump, has_one = authority @ CoordinationError::UnauthorizedAgent )] - pub creator_agent: Account<'info, AgentRegistration>, + pub creator_agent: Box>, /// Wallet-scoped task/dispute rate limit state shared across all agents #[account( @@ -72,7 +72,7 @@ pub struct CreateDependentTask<'info> { seeds = [b"authority_rate_limit", authority.key().as_ref()], bump )] - pub authority_rate_limit: Account<'info, AuthorityRateLimit>, + pub authority_rate_limit: Box>, /// The authority that owns the creator_agent pub authority: Signer<'info>, @@ -142,15 +142,15 @@ pub fn handler( ); let clock = Clock::get()?; - let config = &ctx.accounts.protocol_config; + let config = ctx.accounts.protocol_config.as_ref(); check_version_compatible(config)?; // Validate deadline if set (optional for dependent tasks) validate_deadline(deadline, &clock, false)?; - let creator_agent = &ctx.accounts.creator_agent; - let authority_rate_limit = &mut ctx.accounts.authority_rate_limit; + let creator_agent = ctx.accounts.creator_agent.as_ref(); + let authority_rate_limit = ctx.accounts.authority_rate_limit.as_mut(); // Check wallet-scoped rate limits to prevent multi-agent bypasses under one authority. check_authority_task_creation_rate_limits( @@ -170,7 +170,7 @@ pub fn handler( // depend on a stale view of freshly funded PDAs. let escrow_key = ctx.accounts.escrow.key(); let protocol_fee_bps = config.protocol_fee_bps; - let task = &mut ctx.accounts.task; + let task = ctx.accounts.task.as_mut(); init_task_fields( task, task_id, @@ -313,11 +313,11 @@ pub fn handler( }; // Initialize escrow - let escrow = &mut ctx.accounts.escrow; + let escrow = ctx.accounts.escrow.as_mut(); init_escrow_fields(escrow, task.key(), reward_amount, ctx.bumps.escrow); // Update protocol stats - let protocol_config = &mut ctx.accounts.protocol_config; + let protocol_config = ctx.accounts.protocol_config.as_mut(); increment_total_tasks(protocol_config)?; emit!(DependentTaskCreated { diff --git a/programs/agenc-coordination/src/instructions/create_task.rs b/programs/agenc-coordination/src/instructions/create_task.rs index a0373b34..95e97291 100644 --- a/programs/agenc-coordination/src/instructions/create_task.rs +++ b/programs/agenc-coordination/src/instructions/create_task.rs @@ -26,7 +26,7 @@ pub struct CreateTask<'info> { seeds = [b"task", creator.key().as_ref(), task_id.as_ref()], bump )] - pub task: Account<'info, Task>, + pub task: Box>, #[account( init, @@ -35,14 +35,14 @@ pub struct CreateTask<'info> { seeds = [b"escrow", task.key().as_ref()], bump )] - pub escrow: Account<'info, TaskEscrow>, + pub escrow: Box>, #[account( mut, seeds = [b"protocol"], bump = protocol_config.bump )] - pub protocol_config: Account<'info, ProtocolConfig>, + pub protocol_config: Box>, /// Creator's agent registration for identity/authorization checks #[account( @@ -50,7 +50,7 @@ pub struct CreateTask<'info> { bump = creator_agent.bump, has_one = authority @ CoordinationError::UnauthorizedAgent )] - pub creator_agent: Account<'info, AgentRegistration>, + pub creator_agent: Box>, /// Wallet-scoped task/dispute rate limit state shared across all agents #[account( @@ -60,7 +60,7 @@ pub struct CreateTask<'info> { seeds = [b"authority_rate_limit", authority.key().as_ref()], bump )] - pub authority_rate_limit: Account<'info, AuthorityRateLimit>, + pub authority_rate_limit: Box>, /// The authority that owns the creator_agent pub authority: Signer<'info>, @@ -128,15 +128,15 @@ pub fn handler( require!(reward_amount > 0, CoordinationError::InvalidReward); let clock = Clock::get()?; - let config = &ctx.accounts.protocol_config; + let config = ctx.accounts.protocol_config.as_ref(); check_version_compatible(config)?; // Validate deadline - must be set and in the future (#575) validate_deadline(deadline, &clock, true)?; - let creator_agent = &ctx.accounts.creator_agent; - let authority_rate_limit = &mut ctx.accounts.authority_rate_limit; + let creator_agent = ctx.accounts.creator_agent.as_ref(); + let authority_rate_limit = ctx.accounts.authority_rate_limit.as_mut(); // Check wallet-scoped rate limits to prevent multi-agent bypasses under one authority. check_authority_task_creation_rate_limits( @@ -152,7 +152,7 @@ pub fn handler( // depend on a stale view of freshly funded PDAs. let escrow_key = ctx.accounts.escrow.key(); let protocol_fee_bps = config.protocol_fee_bps; - let task = &mut ctx.accounts.task; + let task = ctx.accounts.task.as_mut(); init_task_fields( task, task_id, @@ -172,7 +172,7 @@ pub fn handler( reward_mint, )?; - let escrow = &mut ctx.accounts.escrow; + let escrow = ctx.accounts.escrow.as_mut(); init_escrow_fields(escrow, task.key(), reward_amount, ctx.bumps.escrow); if let Some(expected_mint) = reward_mint { @@ -290,7 +290,7 @@ pub fn handler( } // Update protocol stats - let protocol_config = &mut ctx.accounts.protocol_config; + let protocol_config = ctx.accounts.protocol_config.as_mut(); increment_total_tasks(protocol_config)?; emit!(TaskCreated { diff --git a/programs/agenc-coordination/src/instructions/initiate_dispute.rs b/programs/agenc-coordination/src/instructions/initiate_dispute.rs index ae91f475..750c373a 100644 --- a/programs/agenc-coordination/src/instructions/initiate_dispute.rs +++ b/programs/agenc-coordination/src/instructions/initiate_dispute.rs @@ -24,7 +24,7 @@ pub struct InitiateDispute<'info> { seeds = [b"dispute", dispute_id.as_ref()], bump )] - pub dispute: Account<'info, Dispute>, + pub dispute: Box>, #[account( mut, @@ -51,30 +51,30 @@ pub struct InitiateDispute<'info> { seeds = [b"authority_rate_limit", authority.key().as_ref()], bump )] - pub authority_rate_limit: Account<'info, AuthorityRateLimit>, + pub authority_rate_limit: Box>, #[account( seeds = [b"protocol"], bump = protocol_config.bump )] - pub protocol_config: Account<'info, ProtocolConfig>, + pub protocol_config: Box>, /// Optional: Initiator's claim if they are a worker (not the creator) #[account( seeds = [b"claim", task.key().as_ref(), agent.key().as_ref()], bump, )] - pub initiator_claim: Option>, + pub initiator_claim: Option>>, /// Optional: Worker agent to be disputed (required when initiator is task creator) #[account(mut)] pub worker_agent: Option>>, /// Optional: Worker's claim (required when worker_agent is provided) - pub worker_claim: Option>, + pub worker_claim: Option>>, /// Optional durable submission record used once the claim slot has been released. - pub task_submission: Option>, + pub task_submission: Option>>, #[account(mut)] pub authority: Signer<'info>, @@ -90,9 +90,9 @@ pub fn handler<'info>( resolution_type: u8, evidence: String, ) -> Result<()> { - let dispute = &mut ctx.accounts.dispute; - let task = &mut ctx.accounts.task; - let config = &ctx.accounts.protocol_config; + let dispute = ctx.accounts.dispute.as_mut(); + let task = ctx.accounts.task.as_mut(); + let config = ctx.accounts.protocol_config.as_ref(); let clock = Clock::get()?; let task_submission = ctx .accounts @@ -215,7 +215,7 @@ pub fn handler<'info>( // Check wallet-scoped rate limits to prevent multi-agent bypasses under one authority. let agent_id = ctx.accounts.agent.agent_id; check_authority_rate_limits( - &mut ctx.accounts.authority_rate_limit, + ctx.accounts.authority_rate_limit.as_mut(), ctx.accounts.authority.key(), ctx.bumps.authority_rate_limit, agent_id, @@ -224,7 +224,7 @@ pub fn handler<'info>( RateLimitAction::DisputeInitiation, )?; - let agent = &mut ctx.accounts.agent; + let agent = ctx.accounts.agent.as_mut(); // === Determine Worker Stake to Snapshot (fix #550) === // Snapshot the worker's stake at dispute initiation time to prevent @@ -316,7 +316,7 @@ pub fn handler<'info>( .worker_agent .as_mut() .ok_or(CoordinationError::WorkerAgentRequired)?; - increment_defendant_counter(defendant_worker)?; + increment_defendant_counter(defendant_worker.as_mut())?; } else { increment_defendant_counter(agent)?; } diff --git a/programs/agenc-coordination/src/instructions/mod.rs b/programs/agenc-coordination/src/instructions/mod.rs index a7222ace..ba4dbf38 100644 --- a/programs/agenc-coordination/src/instructions/mod.rs +++ b/programs/agenc-coordination/src/instructions/mod.rs @@ -65,6 +65,7 @@ pub mod register_skill; pub mod reject_task_result; pub mod resolve_dispute; pub mod revoke_delegation; +pub mod set_task_job_spec; pub mod stake_reputation; pub mod submit_task_result; pub mod suspend_agent; @@ -150,6 +151,8 @@ pub use resolve_dispute::*; #[allow(ambiguous_glob_reexports)] pub use revoke_delegation::*; #[allow(ambiguous_glob_reexports)] +pub use set_task_job_spec::*; +#[allow(ambiguous_glob_reexports)] pub use stake_reputation::*; #[allow(ambiguous_glob_reexports)] pub use submit_task_result::*; diff --git a/programs/agenc-coordination/src/instructions/set_task_job_spec.rs b/programs/agenc-coordination/src/instructions/set_task_job_spec.rs new file mode 100644 index 00000000..33130e80 --- /dev/null +++ b/programs/agenc-coordination/src/instructions/set_task_job_spec.rs @@ -0,0 +1,134 @@ +//! Attach or update a content-addressed job specification pointer for a task. + +use crate::errors::CoordinationError; +use crate::events::TaskJobSpecSet; +use crate::state::{Task, TaskJobSpec, HASH_SIZE, TASK_JOB_SPEC_URI_MAX_LEN}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct SetTaskJobSpec<'info> { + #[account( + seeds = [b"task", task.creator.as_ref(), task.task_id.as_ref()], + bump = task.bump, + constraint = task.creator == creator.key() @ CoordinationError::UnauthorizedTaskAction + )] + pub task: Account<'info, Task>, + + #[account( + init_if_needed, + payer = creator, + space = TaskJobSpec::SIZE, + seeds = [b"task_job_spec", task.key().as_ref()], + bump + )] + pub task_job_spec: Account<'info, TaskJobSpec>, + + #[account(mut)] + pub creator: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, + job_spec_hash: [u8; HASH_SIZE], + job_spec_uri: String, +) -> Result<()> { + validate_task_job_spec_inputs(&job_spec_hash, &job_spec_uri)?; + + let clock = Clock::get()?; + let task_key = ctx.accounts.task.key(); + let task = &ctx.accounts.task; + let task_job_spec = &mut ctx.accounts.task_job_spec; + + if task_job_spec.task != Pubkey::default() { + require!( + task_job_spec.task == task_key, + CoordinationError::TaskJobSpecTaskMismatch + ); + require!( + task_job_spec.creator == task.creator, + CoordinationError::UnauthorizedTaskAction + ); + } + + task_job_spec.task = task_key; + task_job_spec.creator = task.creator; + task_job_spec.job_spec_hash = job_spec_hash; + task_job_spec.job_spec_uri = job_spec_uri.clone(); + if task_job_spec.created_at == 0 { + task_job_spec.created_at = clock.unix_timestamp; + } + task_job_spec.updated_at = clock.unix_timestamp; + task_job_spec.bump = ctx.bumps.task_job_spec; + + emit!(TaskJobSpecSet { + task: task_key, + creator: task.creator, + job_spec_hash, + job_spec_uri, + timestamp: clock.unix_timestamp, + }); + + Ok(()) +} + +pub fn validate_task_job_spec_inputs( + job_spec_hash: &[u8; HASH_SIZE], + job_spec_uri: &str, +) -> Result<()> { + require!( + job_spec_hash.iter().any(|byte| *byte != 0), + CoordinationError::InvalidTaskJobSpecHash + ); + require!( + !job_spec_uri.trim().is_empty(), + CoordinationError::InvalidTaskJobSpecUri + ); + require!( + job_spec_uri.len() <= TASK_JOB_SPEC_URI_MAX_LEN, + CoordinationError::InvalidTaskJobSpecUri + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_non_empty_hash_and_uri() { + let mut hash = [0u8; HASH_SIZE]; + hash[0] = 1; + + assert!(validate_task_job_spec_inputs(&hash, "agenc://job-spec/sha256/abc").is_ok()); + } + + #[test] + fn rejects_zero_hash() { + let err = validate_task_job_spec_inputs(&[0u8; HASH_SIZE], "agenc://job-spec/sha256/abc") + .unwrap_err(); + + assert_eq!(err, CoordinationError::InvalidTaskJobSpecHash.into()); + } + + #[test] + fn rejects_empty_uri() { + let mut hash = [0u8; HASH_SIZE]; + hash[0] = 1; + let err = validate_task_job_spec_inputs(&hash, " \t ").unwrap_err(); + + assert_eq!(err, CoordinationError::InvalidTaskJobSpecUri.into()); + } + + #[test] + fn rejects_oversized_uri() { + let mut hash = [0u8; HASH_SIZE]; + hash[0] = 1; + let uri = "a".repeat(TASK_JOB_SPEC_URI_MAX_LEN + 1); + let err = validate_task_job_spec_inputs(&hash, &uri).unwrap_err(); + + assert_eq!(err, CoordinationError::InvalidTaskJobSpecUri.into()); + } +} diff --git a/programs/agenc-coordination/src/lib.rs b/programs/agenc-coordination/src/lib.rs index ac561fad..7b2c62a0 100644 --- a/programs/agenc-coordination/src/lib.rs +++ b/programs/agenc-coordination/src/lib.rs @@ -127,6 +127,15 @@ pub mod agenc_coordination { ) } + /// Attach or update a content-addressed off-chain job specification pointer for a task. + pub fn set_task_job_spec( + ctx: Context, + job_spec_hash: [u8; 32], + job_spec_uri: String, + ) -> Result<()> { + instructions::set_task_job_spec::handler(ctx, job_spec_hash, job_spec_uri) + } + /// Create a new task that depends on an existing parent task. /// The parent task must not be cancelled or disputed. /// @@ -317,6 +326,11 @@ pub mod agenc_coordination { instructions::claim_task::handler(ctx) } + /// Claim a task only when its content-addressed job specification pointer exists. + pub fn claim_task_with_job_spec(ctx: Context) -> Result<()> { + instructions::claim_task::handler_with_job_spec(ctx) + } + /// Enable Task Validation V2 creator review for an open task. pub fn configure_task_validation( ctx: Context, diff --git a/programs/agenc-coordination/src/state.rs b/programs/agenc-coordination/src/state.rs index 6eb60de4..b3e17822 100644 --- a/programs/agenc-coordination/src/state.rs +++ b/programs/agenc-coordination/src/state.rs @@ -738,6 +738,37 @@ impl TaskValidationVote { pub const SIZE: usize = ::INIT_SPACE.saturating_add(8); } +/// Maximum byte length for a task job specification URI. +pub const TASK_JOB_SPEC_URI_MAX_LEN: usize = 256; + +/// Content-addressed pointer to a task's full off-chain job specification. +/// PDA seeds: ["task_job_spec", task] +#[account] +#[derive(Default, InitSpace)] +pub struct TaskJobSpec { + /// Task this job specification belongs to. + pub task: Pubkey, + /// Task creator authorized to set or update the pointer. + pub creator: Pubkey, + /// SHA-256 hash of the canonicalized job specification payload. + pub job_spec_hash: [u8; HASH_SIZE], + /// URI where the canonicalized job specification payload can be fetched. + #[max_len(256)] + pub job_spec_uri: String, + /// Creation timestamp. + pub created_at: i64, + /// Last update timestamp. + pub updated_at: i64, + /// PDA bump. + pub bump: u8, + /// Reserved for future metadata flags. + pub _reserved: [u8; 7], +} + +impl TaskJobSpec { + pub const SIZE: usize = ::INIT_SPACE.saturating_add(8); +} + /// Task account /// PDA seeds: ["task", creator, task_id] #[account] @@ -1610,6 +1641,11 @@ mod tests { test_size_constant!(Task); } + #[test] + fn test_task_job_spec_size() { + test_size_constant!(TaskJobSpec); + } + #[test] fn test_task_claim_size() { test_size_constant!(TaskClaim);