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:
parent
63a69d0414
commit
85c3024d48
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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"] },
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue