Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/audit/AUDIT_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ The goal of this roadmap is to make the program correct, predictable, and
ready for an on-chain audit before mainnet deployment or SDK integration.


## Current hardening delta

The marketplace job specification flow is part of the active hardening scope.
Runtime claims for marketplace tasks must verify the content-addressed job spec
before signing a claim transaction, and the coordination program exposes a
`claim_task_with_job_spec` path that requires the matching `task_job_spec` PDA.

This is an allowed freeze-period change because it narrows the execution surface:
workers should not claim marketplace work from prompt text or remote payloads
that have not been matched to the on-chain job spec hash and URI. Protocol,
SDK, and runtime changes for this surface must be validated together.


## Guiding principle

No new features are added until correctness is proven. This phase is about reducing risk, not increasing surface area. The program should become boring to read and impossible to surprise.
Expand Down
2 changes: 1 addition & 1 deletion runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"check:executor-baseline": "node scripts/check-executor-baseline.mjs",
"check-idl-drift": "tsx scripts/check-idl-drift.ts",
"pretest": "npm run build --workspace=@tetsuo-ai/desktop-tool-contracts && npm run build --workspace=@tetsuo-ai/plugin-kit-channel-fixture",
"test": "vitest run --exclude tests/integration.test.ts --exclude tests/eval-replay.integration.test.ts --exclude tests/benchmark-runner.integration.test.ts",
"test": "vitest run",
"validate:required": "npx tsx scripts/run-required-validation.ts",
"test:marketplace-integration": "vitest run tests/marketplace-cli.integration.test.ts",
"test:cross-repo-integration": "vitest run tests/integration.test.ts tests/eval-replay.integration.test.ts tests/benchmark-runner.integration.test.ts tests/marketplace-cli.integration.test.ts",
Expand Down
4 changes: 3 additions & 1 deletion runtime/src/channels/discord/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ export class DiscordChannel extends BaseChannelPlugin {
});

client.on("interactionCreate", (interaction: unknown) => {
this.handleInteraction(interaction as DiscordInteraction).catch((err) => {
return this.handleInteraction(
interaction as DiscordInteraction,
).catch((err) => {
this.context.logger.error(
`Error handling interactionCreate: ${errorMessage(err)}`,
);
Expand Down
4 changes: 3 additions & 1 deletion runtime/src/channels/webchat/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,9 @@ async function handleTasksCreate(
}
}
const { program } = await createProgramContext(deps);
const tool = createCreateTaskTool(program, silentLogger);
const tool = createCreateTaskTool(program, silentLogger, {
allowRawTaskCreation: true,
});
const result = await tool.execute(createArgs);
if (result.isError) {
send({ type: 'error', error: `Failed to create task: ${parseToolError(result)}`, id });
Expand Down
20 changes: 13 additions & 7 deletions runtime/src/channels/webchat/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1998,8 +1998,12 @@ describe("WebChatChannel", () => {
),
send,
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(findResponse(send, "watch.cockpit", "req-watch-cockpit")?.payload).toEqual(
const cockpitResponse = await waitForResponse(
send,
"watch.cockpit",
"req-watch-cockpit",
);
expect(cockpitResponse.payload).toEqual(
expect.objectContaining({
session: expect.objectContaining({
sessionId: "session-continuity",
Expand Down Expand Up @@ -2092,18 +2096,20 @@ describe("WebChatChannel", () => {
),
send,
);
await new Promise((resolve) => setTimeout(resolve, 0));

const forkResponse = findResponse(send, "chat.session.fork", "req-fork");
expect(forkResponse?.payload).toEqual(
const forkResponse = await waitForResponse(
send,
"chat.session.fork",
"req-fork",
);
expect(forkResponse.payload).toEqual(
expect.objectContaining({
sourceSessionId: "session-source",
forkSource: "runtime_state",
targetSessionId: expect.any(String),
}),
);

const targetSessionId = (forkResponse?.payload as Record<string, unknown>)
const targetSessionId = (forkResponse.payload as Record<string, unknown>)
.targetSessionId as string;
expect(await store.loadSession(targetSessionId)).toMatchObject({
metadata: {
Expand Down
3 changes: 3 additions & 0 deletions runtime/src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ describe("runtime root CLI", () => {
"Task111111111111111111111111111111111111111",
"--worker-agent-pda",
"Agent11111111111111111111111111111111111111",
"--job-spec-store-dir",
"/tmp/agenc-job-specs",
"--output",
"json",
],
Expand All @@ -797,6 +799,7 @@ describe("runtime root CLI", () => {
expect.objectContaining({
taskPda: "Task111111111111111111111111111111111111111",
workerAgentPda: "Agent11111111111111111111111111111111111111",
jobSpecStoreDir: "/tmp/agenc-job-specs",
}),
);
});
Expand Down
5 changes: 4 additions & 1 deletion runtime/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ const MARKET_COMMAND_OPTIONS: Record<MarketCommand, Set<string>> = {
]),
"tasks.detail": new Set(["job-spec-store-dir"]),
"tasks.cancel": new Set(),
"tasks.claim": new Set(["worker-agent-pda"]),
"tasks.claim": new Set(["worker-agent-pda", "job-spec-store-dir"]),
"tasks.complete": new Set(["proof-hash", "result-data", "worker-agent-pda"]),
"tasks.dispute": new Set([
"evidence",
Expand Down Expand Up @@ -2954,6 +2954,9 @@ function normalizeAndValidateMarketCommand(
...base,
taskPda,
workerAgentPda: parseOptionalStringFlag(parsed.flags["worker-agent-pda"]),
jobSpecStoreDir: parseOptionalStringFlag(
parsed.flags["job-spec-store-dir"],
),
} as MarketTaskClaimOptions;
break;
}
Expand Down
18 changes: 14 additions & 4 deletions runtime/src/cli/marketplace-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,9 +1073,10 @@ export async function runMarketTaskCreateCommand(

try {
const { program } = await createSignerProgramContext(options);
const createTaskOptions = options.jobSpecStoreDir
? { jobSpecStoreDir: options.jobSpecStoreDir }
: undefined;
const createTaskOptions = {
...(options.jobSpecStoreDir ? { jobSpecStoreDir: options.jobSpecStoreDir } : {}),
allowRawTaskCreation: true,
};
const tool = createCreateTaskTool(program, silentLogger, createTaskOptions);
const result = await tool.execute({
description: options.description,
Expand Down Expand Up @@ -1233,7 +1234,16 @@ export async function runMarketTaskClaimCommand(

try {
const { program } = await createSignerProgramContext(options);
const tool = createClaimTaskTool(program, silentLogger);
const tool = createClaimTaskTool(
program,
silentLogger,
options.jobSpecStoreDir
? {
jobSpecStoreDir: options.jobSpecStoreDir,
claimJobSpecVerification: "required",
}
: {},
);
const result = await tool.execute({
taskPda: options.taskPda,
workerAgentPda: options.workerAgentPda,
Expand Down
41 changes: 23 additions & 18 deletions runtime/src/eval/pipeline-http-repro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ export async function runPipelineHttpRepro(
});

const step2 = await runBash(
`cd ${workspace} && python3 -m http.server ${port} >/tmp/agenc-http.log 2>&1 & echo $!`,
[
"set -euo pipefail",
`cd ${workspace}`,
`python3 -m http.server ${port} >/tmp/agenc-http.log 2>&1 &`,
"echo $!",
].join("\n"),
);
if (step2.ok) {
serverPid = step2.stdout.split(/\s+/)[0]?.trim();
Expand Down Expand Up @@ -184,35 +189,35 @@ export async function runPipelineHttpRepro(
preview: preview(step5),
});

const serverPidArg = serverPid && /^\d+$/.test(serverPid) ? serverPid : undefined;
const step6 = await runBash(
[
"if command -v lsof >/dev/null 2>&1; then",
` SERVER_PIDS=$(lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true)`,
' for pid in $SERVER_PIDS; do',
' kill "$pid" >/dev/null 2>&1 || true',
" done",
"elif command -v fuser >/dev/null 2>&1; then",
"set +e",
...(serverPidArg
? [`kill ${serverPidArg} >/dev/null 2>&1 || true`]
: []),
"for _ in 1 2 3 4 5; do",
` if ! curl -fsS --max-time 1 http://127.0.0.1:${port} >/dev/null 2>&1; then echo 0; exit 0; fi`,
" sleep 0.2",
"done",
"if command -v fuser >/dev/null 2>&1; then",
` fuser -k ${port}/tcp >/dev/null 2>&1 || true`,
"else",
` pkill -f 'http.server ${port}' >/dev/null 2>&1 || true`,
"fi",
"sleep 1",
`if command -v ss >/dev/null 2>&1; then`,
` ss -ltn '( sport = :${port} )' | tail -n +2 | wc -l`,
"elif command -v lsof >/dev/null 2>&1; then",
` lsof -nP -iTCP:${port} -sTCP:LISTEN 2>/dev/null | tail -n +2 | wc -l`,
"else",
` if curl -fsS --max-time 1 http://127.0.0.1:${port} >/dev/null 2>&1; then echo 1; else echo 0; fi`,
"if command -v pkill >/dev/null 2>&1; then",
` pkill -f 'http.server ${port}' >/dev/null 2>&1 || true`,
"fi",
"sleep 0.5",
`if curl -fsS --max-time 1 http://127.0.0.1:${port} >/dev/null 2>&1; then echo 1; else echo 0; fi`,
].join("\n"),
);
const portListeners = Number(step6.stdout.trim() || "0");
const listenerStatus = step6.stdout.trim().split(/\s+/).at(-1) ?? "1";
const portListeners = Number(listenerStatus);
steps.push({
step: 6,
tool: "system.bash",
ok: step6.ok && Number.isFinite(portListeners) && portListeners === 0,
preview: step6.ok
? `listeners_on_${port}=${step6.stdout.trim()}`
? `listeners_on_${port}=${listenerStatus}`
: preview(step6),
});

Expand Down
10 changes: 5 additions & 5 deletions runtime/src/gateway/approvals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ describe('ApprovalEngine', () => {
expect(engine.requiresApproval('wallet.transfer', {})).toBeNull();
});

it('matches agenc.createTask with reward > 1 SOL (lamports)', () => {
expect(engine.requiresApproval('agenc.createTask', { reward: 1_000_000_001 })).not.toBeNull();
expect(engine.requiresApproval('agenc.createTask', { reward: 1_000_000_000 })).toBeNull();
it('always requires approval for raw agenc.createTask', () => {
expect(engine.requiresApproval('agenc.createTask', { reward: 1 })).not.toBeNull();
expect(engine.requiresApproval('agenc.createTask', {})).not.toBeNull();
});

it('always requires approval for agenc.registerAgent', () => {
Expand Down Expand Up @@ -1063,9 +1063,9 @@ describe('ApprovalEngine', () => {
expect(rule!.conditions!.minAmount).toBe(0.1);
});

it('agenc.createTask has minAmount 1 SOL in lamports', () => {
it('agenc.createTask has no threshold condition', () => {
const rule = DEFAULT_APPROVAL_RULES.find((r) => r.tool === 'agenc.createTask');
expect(rule!.conditions!.minAmount).toBe(1_000_000_000);
expect(rule!.conditions).toBeUndefined();
});

it('agenc.stakeReputation has minAmount 0.1 SOL in lamports', () => {
Expand Down
3 changes: 1 addition & 2 deletions runtime/src/gateway/approvals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,7 @@ export const DEFAULT_APPROVAL_RULES: readonly ApprovalRule[] = [
},
{
tool: "agenc.createTask",
conditions: { minAmount: 1_000_000_000 },
description: "Task creation with reward exceeding 1 SOL",
description: "Raw marketplace task creation",
},
{
tool: 'agenc.registerAgent',
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/gateway/system-prompt-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ You have broad access to this machine via the system.bash tool.`;
describe("system prompt routing filter", () => {
it("detects when routed tools still require protocol context", () => {
expect(
hasProtocolToolRouting(["system.bash", "agenc.createTask"]),
hasProtocolToolRouting(["system.bash", "agenc.createTaskFromTemplate"]),
).toBe(true);
expect(
hasProtocolToolRouting(["system.bash", "social.sendMessage"]),
Expand Down Expand Up @@ -89,7 +89,7 @@ describe("system prompt routing filter", () => {
it("preserves protocol sections when routed tools include protocol families", () => {
const filtered = filterSystemPromptForToolRouting({
systemPrompt: SYSTEM_PROMPT,
routedToolNames: ["system.bash", "agenc.createTask"],
routedToolNames: ["system.bash", "agenc.createTaskFromTemplate"],
});

expect(filtered).toContain("Solana:");
Expand Down
36 changes: 34 additions & 2 deletions runtime/src/gateway/worktree-isolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ function cloneExecutionLocation(
return JSON.parse(JSON.stringify(location)) as RuntimeExecutionLocation;
}

function getEquivalentPathRoots(path: string): readonly string[] {
const normalizedPath = resolvePath(path);
if (process.platform !== "darwin") {
return [normalizedPath];
}
if (normalizedPath.startsWith("/private/var/")) {
return [normalizedPath, normalizedPath.slice("/private".length)];
}
if (normalizedPath.startsWith("/var/")) {
return [normalizedPath, `/private${normalizedPath}`];
}
return [normalizedPath];
}

function relativeToEquivalentRoot(
path: string,
root: string,
): string | undefined {
for (const rootCandidate of getEquivalentPathRoots(root)) {
for (const pathCandidate of getEquivalentPathRoots(path)) {
if (isPathWithinRoot(pathCandidate, rootCandidate)) {
return relative(rootCandidate, pathCandidate);
}
}
}
return undefined;
}

function translatePathForWorktree(
path: string | undefined,
location: RuntimeExecutionLocation,
Expand All @@ -39,12 +67,16 @@ function translatePathForWorktree(
}
const normalizedPath = resolvePath(path);
const normalizedGitRoot = resolvePath(location.gitRoot);
if (!isPathWithinRoot(normalizedPath, normalizedGitRoot)) {
const relativePath = relativeToEquivalentRoot(
normalizedPath,
normalizedGitRoot,
);
if (relativePath === undefined) {
return normalizedPath;
}
return resolvePath(
location.worktreePath,
relative(normalizedGitRoot, normalizedPath),
relativePath,
);
}

Expand Down
Loading
Loading