fix(runner): constrain repository URLs and defer handoff inputs

Keep create_task at its implemented contract until the dependent backend supports project and plan handoff. Restrict project repository URLs on both semantic surfaces and verify unsafe URL rejection.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-11 17:08:15 -05:00
parent 63a69d0414
commit 85c3024d48
6 changed files with 49 additions and 100 deletions

File diff suppressed because one or more lines are too long

View File

@ -1708,10 +1708,11 @@
"repositoryUrls": {
"description": "Existing HTTPS GitHub repository URLs, including repos absent from the catalog.",
"items": {
"minLength": 1,
"maxLength": 2000,
"pattern": "^https://github\\.com/(?!\\.{1,2}/)[A-Za-z0-9_.-]+/(?!\\.{1,2}/?$)[A-Za-z0-9_.-]+/?$",
"type": "string"
},
"maxItems": 200,
"maxItems": 100,
"type": "array",
"uniqueItems": true
},
@ -1759,7 +1760,7 @@
"standard",
"skill_test"
],
"description": "Create an assigned task. In a conversation, create a project task with no parent; otherwise create a child of the active task. Include initialPlan to persist its plan before execution.",
"description": "Create one child task under the active task.",
"effect": "write",
"inputSchema": {
"additionalProperties": false,
@ -1796,14 +1797,6 @@
"minLength": 1,
"type": "string"
},
"initialPlan": {
"description": "Relevant markdown plan to persist on the new task before it starts.",
"maxLength": 20000,
"type": [
"string",
"null"
]
},
"priority": {
"enum": [
"critical",
@ -1812,16 +1805,8 @@
"low"
]
},
"projectId": {
"description": "Project identifier for the new task.",
"maxLength": 20000,
"type": [
"string",
"null"
]
},
"title": {
"description": "Task title.",
"description": "Child task title.",
"maxLength": 500,
"minLength": 1,
"type": "string"
@ -1888,7 +1873,7 @@
"delegation:tasks:create"
],
"schema": "paperclip.semantic-action.v1",
"title": "Create task",
"title": "Create child task",
"version": 1
},
{

View File

@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js";
import { describe, expect, it } from "vitest";
import { createTaskAction } from "../protocol-actions/create-task.js";
import { createProjectAction } from "../protocol-actions/create-project.js";
import {
PAPERCLIP_SEMANTIC_ACTION_CATALOG,
@ -19,19 +19,21 @@ const packageRoot = resolve(
);
describe("semantic action catalog", () => {
it("accepts project handoff receipts and preserves ordinary child task receipts", () => {
it("limits project repository URLs to HTTPS GitHub repository paths on both tool surfaces", () => {
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true });
const validate = ajv.compile(createTaskAction.live.descriptor.outputSchema);
const receipt = {
commandId: "create-task-1", disposition: "applied", stateRevision: 1,
entityRefs: ["task-1"], scheduledWakeIds: ["wake-1"],
task: { id: "task-1", identifier: "CHAT-1", parentId: null, projectId: "project-1", status: "todo", assigneeActorId: "agent-1" },
};
expect(validate(receipt), JSON.stringify(validate.errors)).toBe(true);
const { projectId: _projectId, ...childTask } = receipt.task;
expect(validate({ ...receipt, task: { ...childTask, parentId: "parent-1" } })).toBe(true);
expect(validate({ ...receipt, task: { ...receipt.task, projectId: 42 } })).toBe(false);
expect(validate({ ...receipt, task: { ...receipt.task, parentId: "" } })).toBe(false);
for (const schema of [createProjectAction.live.descriptor.inputSchema, paperclipSemanticAction("create_project")!.inputSchema]) {
const validate = ajv.compile(schema);
const input = { name: "Project", idempotencyKey: "create-project-1" };
expect(validate({ ...input, repositoryUrls: ["https://github.com/org/repo", "https://github.com/org/other.git/"] })).toBe(true);
for (const url of [
"http://github.com/org/repo", "file:///etc/passwd", "data:text/plain,repo",
"https://localhost/org/repo", "https://127.0.0.1/org/repo", "https://10.0.0.1/org/repo",
"https://github.com.evil.test/org/repo", "https://token@github.com/org/repo",
"https://github.com:8443/org/repo", "https://github.com/org/repo?token=secret",
"https://github.com/org/repo#fragment", "https://github.com/org/repo/tree/main",
"https://github.com/../repo", "https://github.com/org/..",
]) expect(validate({ ...input, repositoryUrls: [url] }), url).toBe(false);
}
});
it("defines one immutable v1 declaration for each Codex-spine action", () => {

View File

@ -6,6 +6,7 @@ import type {
} from "./semantic-action-types.js";
import { searchApiAction } from "../protocol-actions/search-api.js";
import { callApiAction } from "../protocol-actions/call-api.js";
import { projectRepositoryUrlSchema } from "../protocol-actions/create-project.js";
const ALL_MODES = ["standard", "ask", "planning", "skill_test"] as const;
const WORK_MODES = ["standard", "planning", "skill_test"] as const;
@ -451,7 +452,10 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
inputSchema: object({
...idempotency, name: text("Project name.", 500), description: nullableText("Project outcome and context."),
repositoryIds: stringArray("Authorized repository IDs from list_project_repositories; may contain multiple repositories."),
repositoryUrls: stringArray("Existing HTTPS GitHub repository URLs, including repos absent from the catalog."),
repositoryUrls: {
type: "array", items: projectRepositoryUrlSchema, maxItems: 100, uniqueItems: true,
description: "Existing HTTPS GitHub repository URLs, including repos absent from the catalog.",
},
workspace: openObject, status: { enum: ["backlog", "planned", "in_progress", "completed", "cancelled"] },
goalId: nullableText("Goal ID."), goalIds: stringArray("Goal IDs."), leadAgentId: nullableText("Lead agent ID."),
targetDate: nullableText("Target date."), color: nullableText("Project color."), icon: nullableText("Project icon."),
@ -461,8 +465,8 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
}),
descriptor({
operationId: "create_task",
title: "Create task",
description: "Create an assigned task. In a conversation, create a project task with no parent; otherwise create a child of the active task. Include initialPlan to persist its plan before execution.",
title: "Create child task",
description: "Create one child task under the active task.",
placement: "optional",
effect: "write",
requiredClaims: ["delegation:tasks:create"],
@ -470,9 +474,7 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
inputSchema: object(
{
...idempotency,
title: text("Task title.", 500),
projectId: nullableText("Project identifier for the new task."),
initialPlan: nullableText("Relevant markdown plan to persist on the new task before it starts."),
title: text("Child task title.", 500),
description: nullableText("Child task description."),
assigneeActorId: nullableText("Optional actor assignee.", 200),
priority: { enum: ["critical", "high", "medium", "low"] },

View File

@ -1,3 +1,10 @@
/** Existing GitHub repository references, never arbitrary network/resource URIs. */
export const projectRepositoryUrlSchema = {
type: "string",
maxLength: 2000,
pattern: "^https://github\\.com/(?!\\.{1,2}/)[A-Za-z0-9_.-]+/(?!\\.{1,2}/?$)[A-Za-z0-9_.-]+/?$",
} as const;
/** Canonical project tool definition. */
export const createProjectAction = {
"id": "create_project",
@ -173,10 +180,7 @@ export const createProjectAction = {
},
"repositoryUrls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"items": projectRepositoryUrlSchema,
"maxItems": 100,
"description": "Existing HTTPS GitHub repository URLs, including repos absent from the catalog."
}

View File

@ -20,7 +20,7 @@ export const createTaskAction = {
"idempotency": "required",
"disabledByDefault": false,
"realBindingStatus": "live_codex",
"realServiceBinding": "issues.create / issues.createChild",
"realServiceBinding": "issues.createChild",
"prpEvidence": "semantic-operation item event plus company-entity state diff and audit record",
"prpBindingStatus": "bound",
"legacyAliases": [
@ -28,8 +28,8 @@ export const createTaskAction = {
]
},
"documentation": {
"title": "Create task",
"description": "Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution.",
"title": "Create child task",
"description": "Create one durable standard child under the active task.",
"note": null
},
"examples": {
@ -76,8 +76,8 @@ export const createTaskAction = {
"schema": "paperclip.semantic-tool.v1",
"operationId": "create_task",
"version": 1,
"title": "Create task",
"description": "Create a project task from a conversation, or a child from an ordinary task. Persist initialPlan before execution.",
"title": "Create child task",
"description": "Create one durable standard child under the active task. Use only when a real ownership, parallelism, dependency, review, or lifecycle boundary justifies delegation.",
"exposure": "optional",
"requiredClaims": [
"delegation:tasks:create"
@ -134,21 +134,6 @@ export const createTaskAction = {
},
"maxItems": 200,
"uniqueItems": true
},
"projectId": {
"type": [
"string",
"null"
],
"description": "Project ID for the task."
},
"initialPlan": {
"type": [
"string",
"null"
],
"maxLength": 200000,
"description": "Relevant markdown plan saved on the new task before execution starts."
}
},
"required": [
@ -199,42 +184,13 @@ export const createTaskAction = {
"task": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"identifier": {
"type": [
"string",
"null"
]
},
"parentId": {
"type": ["string", "null"],
"minLength": 1
},
"projectId": {
"type": ["string", "null"],
"minLength": 1
},
"status": {
"type": "string",
"minLength": 1
},
"assigneeActorId": {
"type": [
"string",
"null"
]
}
"id": { "type": "string", "minLength": 1 },
"identifier": { "type": ["string", "null"] },
"parentId": { "type": "string", "minLength": 1 },
"status": { "type": "string", "minLength": 1 },
"assigneeActorId": { "type": ["string", "null"] }
},
"required": [
"id",
"identifier",
"parentId",
"status",
"assigneeActorId"
],
"required": ["id", "identifier", "parentId", "status", "assigneeActorId"],
"additionalProperties": false
}
},