a
This commit is contained in:
23
.ai_ops_mock_failure/events/runtime-events.ndjson
Normal file
23
.ai_ops_mock_failure/events/runtime-events.ndjson
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{"id":"e351954e-36c2-48e6-a116-a9e9ef4b58fe","timestamp":"2026-02-24T16:16:29.465Z","type":"session.started","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","message":"Pipeline session started.","metadata":{"entryNodeId":"product-intake"}}
|
||||||
|
{"id":"d63ca8bc-9026-4ece-a599-d96c4179f2d8","timestamp":"2026-02-24T16:16:30.002Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"product-intake","attempt":1,"message":"Node \"product-intake\" attempt 1 completed with status \"success\".","usage":{"tokenInput":84,"tokenOutput":59,"tokenTotal":143,"toolCalls":3,"durationMs":525,"costUsd":0.000286},"metadata":{"status":"success","executionContext":{"phase":"product-intake","modelConstraint":"claude-opus-4-6","allowedTools":["read_file","search","list_files"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"2b1a7fb2-6231-4f0a-9d98-a5fb7e465ccb","timestamp":"2026-02-24T16:16:30.002Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"product-intake","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"8fee1767-4298-4f9b-a2da-7c23bb66297c","timestamp":"2026-02-24T16:16:30.918Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Node \"task-roadmap\" attempt 1 completed with status \"success\".","usage":{"tokenInput":151,"tokenOutput":106,"tokenTotal":257,"toolCalls":21,"durationMs":920,"costUsd":0.000514},"metadata":{"status":"success","executionContext":{"phase":"task-roadmap","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","list_tasks","get_task","create_task","update_task","create_subtask","update_subtask","set_task_dependencies","mcp__claude-task-master__read_file","mcp__claude-task-master__write_file","mcp__claude-task-master__search","mcp__claude-task-master__list_tasks","mcp__claude-task-master__get_task","mcp__claude-task-master__create_task","mcp__claude-task-master__update_task","mcp__claude-task-master__create_subtask","mcp__claude-task-master__update_subtask","mcp__claude-task-master__set_task_dependencies"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"product-intake","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"460d36f6-6f42-4045-ae6e-dd71599e1eb5","timestamp":"2026-02-24T16:16:30.918Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"91e84375-30c3-4cd3-ac0c-83fcfdc600cf","timestamp":"2026-02-24T16:16:30.918Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"task-roadmap","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}}
|
||||||
|
{"id":"18f93340-58a6-452c-94df-8a13006fc0ad","timestamp":"2026-02-24T16:16:31.433Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-a","attempt":1,"message":"Node \"dev-impl-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":124,"tokenOutput":87,"tokenTotal":211,"toolCalls":9,"durationMs":505,"costUsd":0.000422},"metadata":{"status":"success","executionContext":{"phase":"dev-impl-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git_status","git_diff","npm_test"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"task-roadmap","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"6deb1ca9-7d35-4f25-9ce1-03bde8d6676a","timestamp":"2026-02-24T16:16:31.433Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"ac993371-13bc-4d85-b7f3-ba237976095e","timestamp":"2026-02-24T16:16:31.433Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-b","attempt":1,"message":"Node \"dev-impl-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":124,"tokenOutput":87,"tokenTotal":211,"toolCalls":9,"durationMs":505,"costUsd":0.000422},"metadata":{"status":"success","executionContext":{"phase":"dev-impl-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git_status","git_diff","npm_test"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"task-roadmap","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"595457fa-942f-4211-b34b-a34a03810abf","timestamp":"2026-02-24T16:16:31.433Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"dev-impl-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"5b4c50da-48ab-4463-97e9-b92123f9bca8","timestamp":"2026-02-24T16:16:31.831Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Node \"qa-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":99,"tokenOutput":70,"tokenTotal":169,"toolCalls":8,"durationMs":380,"costUsd":0.000338},"metadata":{"status":"success","executionContext":{"phase":"qa-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","npm_test","npm_run"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"dev-impl-b","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"478d0234-271e-42e3-a1fc-579478073970","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"98644a3b-239b-481e-88a6-893f58e3cb15","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.merge_conflict_unresolved","severity":"critical","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Fatal merge error for task \"product-intake\".","metadata":{"source":"pipeline"}}
|
||||||
|
{"id":"222d496f-3155-41d1-8047-54d839097979","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.task_blocked","severity":"critical","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-b","attempt":1,"message":"Task \"product-intake\" blocked due to fatal merge error.","metadata":{"source":"pipeline"}}
|
||||||
|
{"id":"08b2e0fd-1b5e-416d-b482-292d69bcaf20","timestamp":"2026-02-24T16:16:31.831Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Node \"qa-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":99,"tokenOutput":70,"tokenTotal":169,"toolCalls":8,"durationMs":380,"costUsd":0.000338},"metadata":{"status":"success","executionContext":{"phase":"qa-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","npm_test","npm_run"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"parallel","retrySpawned":false,"fromNodeId":"dev-impl-a","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"dce67c2f-269d-422f-9acc-43edc4466492","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"115c4f9d-4f66-4965-8224-aebf3207522a","timestamp":"2026-02-24T16:16:31.831Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"qa-a","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}}
|
||||||
|
{"id":"51ef601c-bcf9-4ec9-a8a9-c062901c20db","timestamp":"2026-02-24T16:16:32.474Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-a","attempt":1,"message":"Node \"merge-a\" attempt 1 completed with status \"success\".","usage":{"tokenInput":93,"tokenOutput":66,"tokenTotal":159,"toolCalls":12,"durationMs":635,"costUsd":0.000318},"metadata":{"status":"success","executionContext":{"phase":"merge-a","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git","git_status","git_diff","git_add","git_commit","git_merge"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"qa-a","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"99d4483e-34e2-4bc4-94e4-b010d0855bae","timestamp":"2026-02-24T16:16:32.474Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-a","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"2f6041ac-aed6-4d7a-93d9-23b13209d6c8","timestamp":"2026-02-24T16:16:33.124Z","type":"node.attempt.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Node \"merge-b\" attempt 1 completed with status \"success\".","usage":{"tokenInput":93,"tokenOutput":66,"tokenTotal":159,"toolCalls":12,"durationMs":635,"costUsd":0.000318},"metadata":{"status":"success","executionContext":{"phase":"merge-b","modelConstraint":"claude-sonnet-4-6","allowedTools":["read_file","write_file","search","list_files","bash","run_command","git","git_status","git_diff","git_add","git_commit","git_merge"],"security":{"dropUid":false,"dropGid":false,"worktreePath":"/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake","violationMode":"hard_abort"}},"topologyKind":"sequential","retrySpawned":false,"fromNodeId":"qa-b","subtasks":["build EcoSwap: Community Skill-Sharing Marketplace","A platform for neighbors to trade time and skills without using actual money","Feature 1: Skill Registry: Users list what they can teach (e"],"securityViolation":false}}
|
||||||
|
{"id":"fc6a654f-b52a-4879-bb69-830a459016a8","timestamp":"2026-02-24T16:16:33.124Z","type":"domain.validation_passed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Node completed successfully.","metadata":{"source":"actor"}}
|
||||||
|
{"id":"35172289-3b25-415a-8355-1948f3efe3d5","timestamp":"2026-02-24T16:16:33.124Z","type":"domain.branch_merged","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","nodeId":"merge-b","attempt":1,"message":"Task \"product-intake\" merged into session base branch.","metadata":{"source":"pipeline"}}
|
||||||
|
{"id":"7c4b5477-89be-45ba-bc81-7fd2109d85d0","timestamp":"2026-02-24T16:16:33.125Z","type":"session.completed","severity":"info","sessionId":"ui-session-mm0t56zh-079824be","message":"Pipeline session completed with status \"success\".","metadata":{"status":"success","recordCount":8,"eventCount":13}}
|
||||||
385
.ai_ops_mock_failure/manifests/structured-dev-workflow.json
Normal file
385
.ai_ops_mock_failure/manifests/structured-dev-workflow.json
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1",
|
||||||
|
"topologies": ["sequential", "parallel"],
|
||||||
|
"personas": [
|
||||||
|
{
|
||||||
|
"id": "product_manager",
|
||||||
|
"displayName": "Product Manager",
|
||||||
|
"systemPromptTemplate": "You are the product manager for {{repo}}. Define clear project goals, user outcomes, non-goals, and acceptance criteria. Maintain a concise PRD-level summary in payload fields so downstream agents can execute independently. Emit a requirements_defined domain event when product requirements are clear enough for implementation planning.",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": ["read_file", "search", "list_files"],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task_manager",
|
||||||
|
"displayName": "Task Manager",
|
||||||
|
"systemPromptTemplate": "You are the task planning agent for {{repo}}. Convert requirements into an implementation roadmap and dependency-aware task graph. Use claude-task-master tooling to create or update tasks, subtasks, and dependencies. Prioritize and surface undone + unblocked tasks first. Required output in payload.taskPlan.tasks: id, title, status, dependencies, subtasks, acceptanceCriteria, and ownerHint. Status values should align with pending/in_progress/blocked/done. Emit requirements_defined when clarifications materially update scope and emit tasks_planned when task graph updates are ready for coding lanes.",
|
||||||
|
"modelConstraint": "claude-sonnet-4-5",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": [
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"search",
|
||||||
|
"list_files",
|
||||||
|
"list_tasks",
|
||||||
|
"get_task",
|
||||||
|
"create_task",
|
||||||
|
"update_task",
|
||||||
|
"create_subtask",
|
||||||
|
"update_subtask",
|
||||||
|
"set_task_dependencies",
|
||||||
|
"mcp__claude-task-master__read_file",
|
||||||
|
"mcp__claude-task-master__write_file",
|
||||||
|
"mcp__claude-task-master__search",
|
||||||
|
"mcp__claude-task-master__list_tasks",
|
||||||
|
"mcp__claude-task-master__get_task",
|
||||||
|
"mcp__claude-task-master__create_task",
|
||||||
|
"mcp__claude-task-master__update_task",
|
||||||
|
"mcp__claude-task-master__create_subtask",
|
||||||
|
"mcp__claude-task-master__update_subtask",
|
||||||
|
"mcp__claude-task-master__set_task_dependencies"
|
||||||
|
],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "developer",
|
||||||
|
"displayName": "Developer",
|
||||||
|
"systemPromptTemplate": "You are a coding agent for {{repo}}. Consume the task plan from handoff payload and select undone, unblocked tasks whose dependencies are satisfied. Keep changes scoped to your assigned lane/node and report completed task IDs. If requirements are unclear or blocked, return status validation_fail with a precise clarification request in payload and emit task_blocked. On successful implementation, emit code_committed and include changedFiles, completedTaskIds, and validation notes in payload.",
|
||||||
|
"modelConstraint": "claude-sonnet-4-5",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": [
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"search",
|
||||||
|
"list_files",
|
||||||
|
"bash",
|
||||||
|
"run_command",
|
||||||
|
"git_status",
|
||||||
|
"git_diff",
|
||||||
|
"npm_test"
|
||||||
|
],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tester",
|
||||||
|
"displayName": "Tester",
|
||||||
|
"systemPromptTemplate": "You are the testing and validation agent for {{repo}}. Validate code and task acceptance criteria for your lane, prioritizing deterministic checks (tests, build, lint, targeted runtime checks). If validation fails, return status validation_fail with reproducible steps and concrete remediation notes. If validation passes, return success, include evidence in payload, and emit validation_passed.",
|
||||||
|
"modelConstraint": "claude-sonnet-4-5",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": [
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"search",
|
||||||
|
"list_files",
|
||||||
|
"bash",
|
||||||
|
"run_command",
|
||||||
|
"npm_test",
|
||||||
|
"npm_run"
|
||||||
|
],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "git_integrator",
|
||||||
|
"displayName": "Git Integrator",
|
||||||
|
"systemPromptTemplate": "You are the git integration agent for {{repo}}. Validate lane readiness for merge into the integration target and ensure the worktree is merge-ready. Run status checks and report risks clearly. Do not force risky merge strategies. On successful readiness checks emit branch_merged and include mergeCommit (or readiness marker), mergedBranch, and targetBranch in payload.",
|
||||||
|
"modelConstraint": "claude-sonnet-4-5",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": [
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"search",
|
||||||
|
"list_files",
|
||||||
|
"bash",
|
||||||
|
"run_command",
|
||||||
|
"git",
|
||||||
|
"git_status",
|
||||||
|
"git_diff",
|
||||||
|
"git_add",
|
||||||
|
"git_commit",
|
||||||
|
"git_merge"
|
||||||
|
],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push", "git_rebase"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "conflict_resolver",
|
||||||
|
"displayName": "Conflict Resolver",
|
||||||
|
"systemPromptTemplate": "You are the merge conflict resolver for {{repo}}. Resolve conflict markers for the assigned task/worktree, run targeted validation checks, and return success only when conflicts are cleanly resolved. Include resolvedFiles and validation evidence in payload.",
|
||||||
|
"modelConstraint": "claude-sonnet-4-5",
|
||||||
|
"toolClearance": {
|
||||||
|
"allowlist": [
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"search",
|
||||||
|
"list_files",
|
||||||
|
"bash",
|
||||||
|
"run_command",
|
||||||
|
"git",
|
||||||
|
"git_status",
|
||||||
|
"git_diff",
|
||||||
|
"git_add",
|
||||||
|
"git_commit",
|
||||||
|
"npm_test",
|
||||||
|
"npm_run"
|
||||||
|
],
|
||||||
|
"banlist": ["delete_file", "rm", "git_reset", "git_clean", "git_push", "git_rebase"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": [
|
||||||
|
{
|
||||||
|
"parentPersonaId": "product_manager",
|
||||||
|
"childPersonaId": "task_manager",
|
||||||
|
"constraints": {
|
||||||
|
"maxDepth": 2,
|
||||||
|
"maxChildren": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parentPersonaId": "task_manager",
|
||||||
|
"childPersonaId": "developer",
|
||||||
|
"constraints": {
|
||||||
|
"maxDepth": 4,
|
||||||
|
"maxChildren": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parentPersonaId": "task_manager",
|
||||||
|
"childPersonaId": "tester",
|
||||||
|
"constraints": {
|
||||||
|
"maxDepth": 4,
|
||||||
|
"maxChildren": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parentPersonaId": "task_manager",
|
||||||
|
"childPersonaId": "git_integrator",
|
||||||
|
"constraints": {
|
||||||
|
"maxDepth": 5,
|
||||||
|
"maxChildren": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"topologyConstraints": {
|
||||||
|
"maxDepth": 6,
|
||||||
|
"maxRetries": 0
|
||||||
|
},
|
||||||
|
"pipeline": {
|
||||||
|
"entryNodeId": "product-intake",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "product-intake",
|
||||||
|
"actorId": "product_manager_actor",
|
||||||
|
"personaId": "product_manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task-roadmap",
|
||||||
|
"actorId": "task_manager_actor",
|
||||||
|
"personaId": "task_manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-impl-a",
|
||||||
|
"actorId": "developer_actor",
|
||||||
|
"personaId": "developer",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "implementation-pass-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-impl-b",
|
||||||
|
"actorId": "developer_actor",
|
||||||
|
"personaId": "developer",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "implementation-pass-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa-a",
|
||||||
|
"actorId": "tester_actor",
|
||||||
|
"personaId": "tester",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "validation-pass-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa-b",
|
||||||
|
"actorId": "tester_actor",
|
||||||
|
"personaId": "tester",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "validation-pass-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task-clarify-a",
|
||||||
|
"actorId": "task_manager_actor",
|
||||||
|
"personaId": "task_manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task-clarify-b",
|
||||||
|
"actorId": "task_manager_actor",
|
||||||
|
"personaId": "task_manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-rework-a",
|
||||||
|
"actorId": "developer_actor",
|
||||||
|
"personaId": "developer",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "implementation-pass-2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev-rework-b",
|
||||||
|
"actorId": "developer_actor",
|
||||||
|
"personaId": "developer",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "implementation-pass-2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa-rework-a",
|
||||||
|
"actorId": "tester_actor",
|
||||||
|
"personaId": "tester",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "validation-pass-2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qa-rework-b",
|
||||||
|
"actorId": "tester_actor",
|
||||||
|
"personaId": "tester",
|
||||||
|
"topology": {
|
||||||
|
"kind": "parallel",
|
||||||
|
"blockId": "validation-pass-2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "merge-a",
|
||||||
|
"actorId": "git_integrator_actor",
|
||||||
|
"personaId": "git_integrator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "merge-b",
|
||||||
|
"actorId": "git_integrator_actor",
|
||||||
|
"personaId": "git_integrator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "merge-conflict-resolve-a",
|
||||||
|
"actorId": "conflict_resolver_actor",
|
||||||
|
"personaId": "conflict_resolver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "merge-conflict-resolve-b",
|
||||||
|
"actorId": "conflict_resolver_actor",
|
||||||
|
"personaId": "conflict_resolver"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "product-intake",
|
||||||
|
"to": "task-roadmap",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "task-roadmap",
|
||||||
|
"to": "dev-impl-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "task-roadmap",
|
||||||
|
"to": "dev-impl-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-impl-a",
|
||||||
|
"to": "qa-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-impl-b",
|
||||||
|
"to": "qa-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-impl-a",
|
||||||
|
"to": "task-clarify-a",
|
||||||
|
"on": "validation_fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-impl-b",
|
||||||
|
"to": "task-clarify-b",
|
||||||
|
"on": "validation_fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-a",
|
||||||
|
"to": "dev-rework-a",
|
||||||
|
"on": "validation_fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-b",
|
||||||
|
"to": "dev-rework-b",
|
||||||
|
"on": "validation_fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "task-clarify-a",
|
||||||
|
"to": "dev-rework-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "task-clarify-b",
|
||||||
|
"to": "dev-rework-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-rework-a",
|
||||||
|
"to": "qa-rework-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dev-rework-b",
|
||||||
|
"to": "qa-rework-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-a",
|
||||||
|
"to": "merge-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-b",
|
||||||
|
"to": "merge-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-rework-a",
|
||||||
|
"to": "merge-a",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "qa-rework-b",
|
||||||
|
"to": "merge-b",
|
||||||
|
"on": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "merge-a",
|
||||||
|
"to": "merge-conflict-resolve-a",
|
||||||
|
"event": "merge_conflict_detected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "merge-b",
|
||||||
|
"to": "merge-conflict-resolve-b",
|
||||||
|
"event": "merge_conflict_detected"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "dev-impl-a",
|
||||||
|
"fromNodeId": "task-roadmap",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node task-roadmap completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "merged"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:30.919Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "dev-impl-b",
|
||||||
|
"fromNodeId": "task-roadmap",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node task-roadmap completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "merged"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:30.919Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "merge-a",
|
||||||
|
"fromNodeId": "qa-a",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node qa-a completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "merged"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:31.832Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "merge-b",
|
||||||
|
"fromNodeId": "qa-b",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node qa-b completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "fatal_error",
|
||||||
|
"mergeError": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:31.832Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "product-intake",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned."
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:29.466Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "qa-a",
|
||||||
|
"fromNodeId": "dev-impl-a",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node dev-impl-a completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "merged"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:31.434Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "qa-b",
|
||||||
|
"fromNodeId": "dev-impl-b",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node dev-impl-b completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"mergeStatus": "merged"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:31.434Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"nodeId": "task-roadmap",
|
||||||
|
"fromNodeId": "product-intake",
|
||||||
|
"payload": {
|
||||||
|
"prompt": "build EcoSwap: Community Skill-Sharing Marketplace\nA platform for neighbors to trade time and skills without using actual money.\n\nFeature 1: Skill Registry: Users list what they can teach (e.g., \"Basic Plumbing\") and what they want to learn.\n\nFeature 2: Time-Bank Ledger: A digital currency where 1 hour of work = 1 Credit.\n\nFeature 3: Scheduling Bridge: An integrated calendar to book \"Swap Sessions.\"\n\nFeature 4: Trust/Rating System: Reviews that directly affect a user’s \"Reliability Tier.\"\n\nFeature 5: Dispute Resolution: A community-led voting system for when a \"Swap\" doesn't go as planned.",
|
||||||
|
"summary": "Node product-intake completed in mock mode.",
|
||||||
|
"subtasks": [
|
||||||
|
"build EcoSwap: Community Skill-Sharing Marketplace",
|
||||||
|
"A platform for neighbors to trade time and skills without using actual money",
|
||||||
|
"Feature 1: Skill Registry: Users list what they can teach (e"
|
||||||
|
],
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake"
|
||||||
|
},
|
||||||
|
"createdAt": "2026-02-24T16:16:30.002Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"globalFlags": {},
|
||||||
|
"artifactPointers": {
|
||||||
|
"sessions/ui-session-mm0t56zh-079824be/last_completed_node": "merge-b",
|
||||||
|
"sessions/ui-session-mm0t56zh-079824be/last_attempt": "1",
|
||||||
|
"sessions/ui-session-mm0t56zh-079824be/final_state": "/home/zman/projects/ai_ops/.ai_ops/state/ui-session-mm0t56zh-079824be/state.json"
|
||||||
|
},
|
||||||
|
"taskQueue": [
|
||||||
|
{
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"id": "product-intake",
|
||||||
|
"title": "product-intake",
|
||||||
|
"status": "merged",
|
||||||
|
"metadata": {
|
||||||
|
"mergeConflict": {
|
||||||
|
"attempts": 0,
|
||||||
|
"maxAttempts": 2,
|
||||||
|
"status": "merged",
|
||||||
|
"mergedAt": "2026-02-24T16:16:33.124Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"sessionId": "ui-session-mm0t56zh-079824be",
|
||||||
|
"projectPath": "/home/zman/projects/ai_ops/.workspace/ecoswap",
|
||||||
|
"baseWorkspacePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base",
|
||||||
|
"sessionStatus": "active",
|
||||||
|
"createdAt": "2026-02-24T16:16:12.702Z",
|
||||||
|
"updatedAt": "2026-02-24T16:16:12.702Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"flags": {
|
||||||
|
"product-intake_completed": true,
|
||||||
|
"task-roadmap_completed": true,
|
||||||
|
"dev-impl-a_completed": true,
|
||||||
|
"dev-impl-b_completed": true,
|
||||||
|
"qa-b_completed": true,
|
||||||
|
"qa-a_completed": true,
|
||||||
|
"merge-a_completed": true,
|
||||||
|
"merge-b_completed": true
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"project_context": {
|
||||||
|
"globalFlags": {},
|
||||||
|
"artifactPointers": {},
|
||||||
|
"taskQueue": []
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"tokenInput": 93,
|
||||||
|
"tokenOutput": 66,
|
||||||
|
"durationMs": 635,
|
||||||
|
"costUsd": 0.000318,
|
||||||
|
"tokenTotal": 159,
|
||||||
|
"toolCalls": 12
|
||||||
|
},
|
||||||
|
"topologyHint": "manifest-default"
|
||||||
|
},
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"nodeId": "product-intake",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:30.000Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "task-roadmap",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:30.905Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "task-roadmap",
|
||||||
|
"event": "branch_merged",
|
||||||
|
"timestamp": "2026-02-24T16:16:30.918Z",
|
||||||
|
"data": {
|
||||||
|
"source": "pipeline",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Task \"product-intake\" merged into session base branch.",
|
||||||
|
"details": {
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "dev-impl-a",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.432Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "dev-impl-b",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.432Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "qa-b",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.817Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "qa-b",
|
||||||
|
"event": "merge_conflict_unresolved",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.831Z",
|
||||||
|
"data": {
|
||||||
|
"source": "pipeline",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Fatal merge error for task \"product-intake\".",
|
||||||
|
"details": {
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake",
|
||||||
|
"error": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree",
|
||||||
|
"mergeBase": "1650156ff0966f9071107c1e3ed4e51d02df3f24"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "qa-b",
|
||||||
|
"event": "task_blocked",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.831Z",
|
||||||
|
"data": {
|
||||||
|
"source": "pipeline",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Task \"product-intake\" blocked due to fatal merge error.",
|
||||||
|
"details": {
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"error": "git -C /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/base worktree remove --force /home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake failed: fatal: '/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake' is not a working tree"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "qa-a",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.816Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "qa-a",
|
||||||
|
"event": "branch_merged",
|
||||||
|
"timestamp": "2026-02-24T16:16:31.831Z",
|
||||||
|
"data": {
|
||||||
|
"source": "pipeline",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Task \"product-intake\" merged into session base branch.",
|
||||||
|
"details": {
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "merge-a",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:32.473Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "merge-b",
|
||||||
|
"event": "validation_passed",
|
||||||
|
"timestamp": "2026-02-24T16:16:33.112Z",
|
||||||
|
"data": {
|
||||||
|
"source": "actor",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Node completed successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "merge-b",
|
||||||
|
"event": "branch_merged",
|
||||||
|
"timestamp": "2026-02-24T16:16:33.124Z",
|
||||||
|
"data": {
|
||||||
|
"source": "pipeline",
|
||||||
|
"attempt": 1,
|
||||||
|
"summary": "Task \"product-intake\" merged into session base branch.",
|
||||||
|
"details": {
|
||||||
|
"taskId": "product-intake",
|
||||||
|
"worktreePath": "/home/zman/projects/ai_ops/.ai_ops/worktrees/ui-session-mm0t56zh-079824be/tasks/product-intake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"runId": "eb9f2e22-d117-42fd-b03c-25a08127a3d8",
|
||||||
|
"sessionId": "ui-session-mm0t56zh-079824be",
|
||||||
|
"status": "success",
|
||||||
|
"startedAt": "2026-02-24T16:16:29.463Z",
|
||||||
|
"executionMode": "mock",
|
||||||
|
"provider": "claude",
|
||||||
|
"manifestPath": ".ai_ops/manifests/structured-dev-workflow.json",
|
||||||
|
"endedAt": "2026-02-24T16:16:33.125Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# ECOSWAP
|
||||||
@@ -16,6 +16,7 @@ CLAUDE_CODE_OAUTH_TOKEN=
|
|||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
CLAUDE_MODEL=
|
CLAUDE_MODEL=
|
||||||
CLAUDE_CODE_PATH=
|
CLAUDE_CODE_PATH=
|
||||||
|
CLAUDE_MAX_TURNS=2
|
||||||
# Claude binary observability: off | stdout | file | both
|
# Claude binary observability: off | stdout | file | both
|
||||||
CLAUDE_OBSERVABILITY_MODE=off
|
CLAUDE_OBSERVABILITY_MODE=off
|
||||||
# CLAUDE_OBSERVABILITY_VERBOSITY: summary | full
|
# CLAUDE_OBSERVABILITY_VERBOSITY: summary | full
|
||||||
@@ -52,7 +53,7 @@ AGENT_PORT_LOCK_DIR=.ai_ops/locks/ports
|
|||||||
AGENT_DISCOVERY_FILE_RELATIVE_PATH=.agent-context/resources.json
|
AGENT_DISCOVERY_FILE_RELATIVE_PATH=.agent-context/resources.json
|
||||||
|
|
||||||
# Security middleware
|
# Security middleware
|
||||||
# AGENT_SECURITY_VIOLATION_MODE: hard_abort | validation_fail
|
# AGENT_SECURITY_VIOLATION_MODE: hard_abort | validation_fail | dangerous_warn_only
|
||||||
AGENT_SECURITY_VIOLATION_MODE=hard_abort
|
AGENT_SECURITY_VIOLATION_MODE=hard_abort
|
||||||
AGENT_SECURITY_ALLOWED_BINARIES=git,npm,node,cat,ls,pwd,echo,bash,sh
|
AGENT_SECURITY_ALLOWED_BINARIES=git,npm,node,cat,ls,pwd,echo,bash,sh
|
||||||
AGENT_SECURITY_COMMAND_TIMEOUT_MS=120000
|
AGENT_SECURITY_COMMAND_TIMEOUT_MS=120000
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ Provider mode notes:
|
|||||||
- `provider=codex` uses existing OpenAI/Codex auth settings (`OPENAI_AUTH_MODE`, `CODEX_API_KEY`, `OPENAI_API_KEY`).
|
- `provider=codex` uses existing OpenAI/Codex auth settings (`OPENAI_AUTH_MODE`, `CODEX_API_KEY`, `OPENAI_API_KEY`).
|
||||||
- `provider=claude` uses Claude auth resolution (`CLAUDE_CODE_OAUTH_TOKEN` preferred, otherwise `ANTHROPIC_API_KEY`, or existing Claude Code login state).
|
- `provider=claude` uses Claude auth resolution (`CLAUDE_CODE_OAUTH_TOKEN` preferred, otherwise `ANTHROPIC_API_KEY`, or existing Claude Code login state).
|
||||||
- `CLAUDE_MODEL` should be a Claude model id/alias recognized by Claude Code (for example `claude-sonnet-4-6`); `anthropic/...` prefixes are normalized automatically.
|
- `CLAUDE_MODEL` should be a Claude model id/alias recognized by Claude Code (for example `claude-sonnet-4-6`); `anthropic/...` prefixes are normalized automatically.
|
||||||
|
- `CLAUDE_MAX_TURNS` controls the per-query Claude turn budget (default `2`).
|
||||||
- Claude provider runs can emit Claude SDK/CLI internals to stdout and/or NDJSON with `CLAUDE_OBSERVABILITY_*` settings.
|
- Claude provider runs can emit Claude SDK/CLI internals to stdout and/or NDJSON with `CLAUDE_OBSERVABILITY_*` settings.
|
||||||
|
- UI session-mode provider runs execute directly in orchestration-assigned task/base worktrees; provider adapters do not allocate additional nested worktrees.
|
||||||
|
|
||||||
## Manifest Semantics
|
## Manifest Semantics
|
||||||
|
|
||||||
@@ -271,6 +273,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson
|
|||||||
- Pipeline behavior on `SecurityViolationError` is configurable:
|
- Pipeline behavior on `SecurityViolationError` is configurable:
|
||||||
- `hard_abort` (default)
|
- `hard_abort` (default)
|
||||||
- `validation_fail` (retry-unrolled remediation)
|
- `validation_fail` (retry-unrolled remediation)
|
||||||
|
- `dangerous_warn_only` (logs violations and continues execution; high risk)
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -285,6 +288,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson
|
|||||||
- `ANTHROPIC_API_KEY` (used when `CLAUDE_CODE_OAUTH_TOKEN` is unset)
|
- `ANTHROPIC_API_KEY` (used when `CLAUDE_CODE_OAUTH_TOKEN` is unset)
|
||||||
- `CLAUDE_MODEL`
|
- `CLAUDE_MODEL`
|
||||||
- `CLAUDE_CODE_PATH`
|
- `CLAUDE_CODE_PATH`
|
||||||
|
- `CLAUDE_MAX_TURNS` (integer >= 1, defaults to `2`)
|
||||||
- `CLAUDE_OBSERVABILITY_MODE` (`off`, `stdout`, `file`, or `both`)
|
- `CLAUDE_OBSERVABILITY_MODE` (`off`, `stdout`, `file`, or `both`)
|
||||||
- `CLAUDE_OBSERVABILITY_VERBOSITY` (`summary` or `full`)
|
- `CLAUDE_OBSERVABILITY_VERBOSITY` (`summary` or `full`)
|
||||||
- `CLAUDE_OBSERVABILITY_LOG_PATH`
|
- `CLAUDE_OBSERVABILITY_LOG_PATH`
|
||||||
@@ -322,7 +326,7 @@ jq -c 'select(.severity=="critical")' .ai_ops/events/runtime-events.ndjson
|
|||||||
|
|
||||||
### Security Middleware
|
### Security Middleware
|
||||||
|
|
||||||
- `AGENT_SECURITY_VIOLATION_MODE` (`hard_abort` or `validation_fail`)
|
- `AGENT_SECURITY_VIOLATION_MODE` (`hard_abort`, `validation_fail`, or `dangerous_warn_only`)
|
||||||
- `AGENT_SECURITY_ALLOWED_BINARIES`
|
- `AGENT_SECURITY_ALLOWED_BINARIES`
|
||||||
- `AGENT_SECURITY_COMMAND_TIMEOUT_MS`
|
- `AGENT_SECURITY_COMMAND_TIMEOUT_MS`
|
||||||
- `AGENT_SECURITY_AUDIT_LOG_PATH`
|
- `AGENT_SECURITY_AUDIT_LOG_PATH`
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ Before each actor invocation, orchestration resolves an immutable `ResolvedExecu
|
|||||||
|
|
||||||
This keeps orchestration policy resolution separate from executor enforcement. Executors do not need to parse manifests or MCP registry internals.
|
This keeps orchestration policy resolution separate from executor enforcement. Executors do not need to parse manifests or MCP registry internals.
|
||||||
|
|
||||||
|
Worktree ownership invariant:
|
||||||
|
|
||||||
|
- In UI session mode, orchestration/session lifecycle is the single owner of git worktree allocation.
|
||||||
|
- Provider adapters (Codex/Claude runtime wrappers) must execute inside `ResolvedExecutionContext.security.worktreePath` and must not provision independent worktrees.
|
||||||
|
|
||||||
## Execution topology model
|
## Execution topology model
|
||||||
|
|
||||||
- Pipeline graph execution is DAG-based with ready-node frontiers.
|
- Pipeline graph execution is DAG-based with ready-node frontiers.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ This middleware provides a first-pass hardening layer for agent-executed shell c
|
|||||||
|
|
||||||
- `hard_abort` (default): fail fast and stop the pipeline.
|
- `hard_abort` (default): fail fast and stop the pipeline.
|
||||||
- `validation_fail`: map violation to retry-unrolled behavior so the actor can attempt a compliant alternative.
|
- `validation_fail`: map violation to retry-unrolled behavior so the actor can attempt a compliant alternative.
|
||||||
|
- `dangerous_warn_only`: emit security audit/runtime events but continue execution. This is intentionally unsafe and should only be used for temporary unblock/debug workflows.
|
||||||
|
|
||||||
## MCP integration
|
## MCP integration
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { JsonObject } from "./types.js";
|
|||||||
import { SessionWorktreeManager, type SessionMetadata } from "./session-lifecycle.js";
|
import { SessionWorktreeManager, type SessionMetadata } from "./session-lifecycle.js";
|
||||||
import {
|
import {
|
||||||
SecureCommandExecutor,
|
SecureCommandExecutor,
|
||||||
|
type SecurityViolationHandling,
|
||||||
type SecurityAuditEvent,
|
type SecurityAuditEvent,
|
||||||
type SecurityAuditSink,
|
type SecurityAuditSink,
|
||||||
SecurityRulesEngine,
|
SecurityRulesEngine,
|
||||||
@@ -46,7 +47,7 @@ export type OrchestrationSettings = {
|
|||||||
maxRetries: number;
|
maxRetries: number;
|
||||||
maxChildren: number;
|
maxChildren: number;
|
||||||
mergeConflictMaxAttempts: number;
|
mergeConflictMaxAttempts: number;
|
||||||
securityViolationHandling: "hard_abort" | "validation_fail";
|
securityViolationHandling: SecurityViolationHandling;
|
||||||
runtimeContext: Record<string, string | number | boolean>;
|
runtimeContext: Record<string, string | number | boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,6 +212,9 @@ function createActorSecurityContext(input: {
|
|||||||
blockedEnvAssignments: ["AGENT_STATE_ROOT", "AGENT_PROJECT_CONTEXT_PATH"],
|
blockedEnvAssignments: ["AGENT_STATE_ROOT", "AGENT_PROJECT_CONTEXT_PATH"],
|
||||||
},
|
},
|
||||||
auditSink,
|
auditSink,
|
||||||
|
{
|
||||||
|
violationHandling: input.settings.securityViolationHandling,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -342,6 +346,7 @@ export class SchemaDrivenExecutionEngine {
|
|||||||
this.sessionWorktreeManager = new SessionWorktreeManager({
|
this.sessionWorktreeManager = new SessionWorktreeManager({
|
||||||
worktreeRoot: resolve(this.settings.workspaceRoot, this.config.provisioning.gitWorktree.rootDirectory),
|
worktreeRoot: resolve(this.settings.workspaceRoot, this.config.provisioning.gitWorktree.rootDirectory),
|
||||||
baseRef: this.config.provisioning.gitWorktree.baseRef,
|
baseRef: this.config.provisioning.gitWorktree.baseRef,
|
||||||
|
targetPath: this.config.provisioning.gitWorktree.targetPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actorExecutors = toExecutorMap(input.actorExecutors);
|
this.actorExecutors = toExecutorMap(input.actorExecutors);
|
||||||
@@ -426,7 +431,11 @@ export class SchemaDrivenExecutionEngine {
|
|||||||
}): Promise<PipelineRunSummary> {
|
}): Promise<PipelineRunSummary> {
|
||||||
const managerSessionId = `${input.sessionId}__pipeline`;
|
const managerSessionId = `${input.sessionId}__pipeline`;
|
||||||
const managerSession = this.manager.createSession(managerSessionId);
|
const managerSession = this.manager.createSession(managerSessionId);
|
||||||
const workspaceRoot = input.sessionMetadata?.baseWorkspacePath ?? this.settings.workspaceRoot;
|
const workspaceRoot = input.sessionMetadata
|
||||||
|
? this.sessionWorktreeManager.resolveWorkingDirectoryForWorktree(
|
||||||
|
input.sessionMetadata.baseWorkspacePath,
|
||||||
|
)
|
||||||
|
: this.settings.workspaceRoot;
|
||||||
const projectContextStore = input.sessionMetadata
|
const projectContextStore = input.sessionMetadata
|
||||||
? new FileSystemProjectContextStore({
|
? new FileSystemProjectContextStore({
|
||||||
filePath: resolveSessionProjectContextPath(this.settings.stateRoot, input.sessionId),
|
filePath: resolveSessionProjectContextPath(this.settings.stateRoot, input.sessionId),
|
||||||
@@ -531,6 +540,7 @@ export class SchemaDrivenExecutionEngine {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
taskId,
|
taskId,
|
||||||
|
workingDirectory: ensured.taskWorkingDirectory,
|
||||||
worktreePath: ensured.taskWorktreePath,
|
worktreePath: ensured.taskWorktreePath,
|
||||||
statusAtStart,
|
statusAtStart,
|
||||||
...(existing?.metadata ? { metadata: existing.metadata } : {}),
|
...(existing?.metadata ? { metadata: existing.metadata } : {}),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export type ActorExecutionResult = {
|
|||||||
export type ActorToolPermissionResult =
|
export type ActorToolPermissionResult =
|
||||||
| {
|
| {
|
||||||
behavior: "allow";
|
behavior: "allow";
|
||||||
|
updatedInput?: Record<string, unknown>;
|
||||||
toolUseID?: string;
|
toolUseID?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -171,6 +172,7 @@ export type ActorExecutionSecurityContext = {
|
|||||||
|
|
||||||
export type TaskExecutionResolution = {
|
export type TaskExecutionResolution = {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
|
workingDirectory: string;
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
statusAtStart: string;
|
statusAtStart: string;
|
||||||
metadata?: JsonObject;
|
metadata?: JsonObject;
|
||||||
@@ -941,7 +943,7 @@ export class PipelineExecutor {
|
|||||||
node,
|
node,
|
||||||
toolClearance,
|
toolClearance,
|
||||||
prompt,
|
prompt,
|
||||||
worktreePathOverride: taskResolution?.worktreePath,
|
worktreePathOverride: taskResolution?.workingDirectory,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await this.invokeActorExecutor({
|
const result = await this.invokeActorExecutor({
|
||||||
@@ -970,6 +972,7 @@ export class PipelineExecutor {
|
|||||||
...(taskResolution
|
...(taskResolution
|
||||||
? {
|
? {
|
||||||
taskId: taskResolution.taskId,
|
taskId: taskResolution.taskId,
|
||||||
|
workingDirectory: taskResolution.workingDirectory,
|
||||||
worktreePath: taskResolution.worktreePath,
|
worktreePath: taskResolution.worktreePath,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -1309,6 +1312,7 @@ export class PipelineExecutor {
|
|||||||
const createToolPermissionHandler = (): ActorToolPermissionHandler =>
|
const createToolPermissionHandler = (): ActorToolPermissionHandler =>
|
||||||
this.createToolPermissionHandler({
|
this.createToolPermissionHandler({
|
||||||
allowedTools: executionContext.allowedTools,
|
allowedTools: executionContext.allowedTools,
|
||||||
|
violationMode: executionContext.security.violationMode,
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
nodeId: input.nodeId,
|
nodeId: input.nodeId,
|
||||||
attempt: input.attempt,
|
attempt: input.attempt,
|
||||||
@@ -1326,6 +1330,7 @@ export class PipelineExecutor {
|
|||||||
|
|
||||||
private createToolPermissionHandler(input: {
|
private createToolPermissionHandler(input: {
|
||||||
allowedTools: readonly string[];
|
allowedTools: readonly string[];
|
||||||
|
violationMode: SecurityViolationHandling;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
attempt: number;
|
attempt: number;
|
||||||
@@ -1340,7 +1345,7 @@ export class PipelineExecutor {
|
|||||||
attempt: input.attempt,
|
attempt: input.attempt,
|
||||||
};
|
};
|
||||||
|
|
||||||
return async (toolName, _input, options) => {
|
return async (toolName, toolInput, options) => {
|
||||||
const toolUseID = options.toolUseID;
|
const toolUseID = options.toolUseID;
|
||||||
if (options.signal.aborted) {
|
if (options.signal.aborted) {
|
||||||
return {
|
return {
|
||||||
@@ -1358,11 +1363,28 @@ export class PipelineExecutor {
|
|||||||
caseInsensitiveLookup: caseInsensitiveAllowLookup,
|
caseInsensitiveLookup: caseInsensitiveAllowLookup,
|
||||||
});
|
});
|
||||||
if (!allowMatch) {
|
if (!allowMatch) {
|
||||||
rulesEngine?.assertToolInvocationAllowed({
|
if (rulesEngine) {
|
||||||
|
try {
|
||||||
|
rulesEngine.assertToolInvocationAllowed({
|
||||||
tool: candidates[0] ?? toolName,
|
tool: candidates[0] ?? toolName,
|
||||||
toolClearance: toolPolicy,
|
toolClearance: toolPolicy,
|
||||||
context: toolAuditContext,
|
context: toolAuditContext,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
!(input.violationMode === "dangerous_warn_only" && error instanceof SecurityViolationError)
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input.violationMode === "dangerous_warn_only") {
|
||||||
|
return {
|
||||||
|
behavior: "allow",
|
||||||
|
updatedInput: toolInput,
|
||||||
|
...(toolUseID ? { toolUseID } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
behavior: "deny",
|
behavior: "deny",
|
||||||
message: `Tool "${toolName}" is not in the resolved execution allowlist.`,
|
message: `Tool "${toolName}" is not in the resolved execution allowlist.`,
|
||||||
@@ -1379,6 +1401,7 @@ export class PipelineExecutor {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
behavior: "allow",
|
behavior: "allow",
|
||||||
|
updatedInput: toolInput,
|
||||||
...(toolUseID ? { toolUseID } : {}),
|
...(toolUseID ? { toolUseID } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -358,13 +358,16 @@ export class FileSystemSessionMetadataStore {
|
|||||||
export class SessionWorktreeManager {
|
export class SessionWorktreeManager {
|
||||||
private readonly worktreeRoot: string;
|
private readonly worktreeRoot: string;
|
||||||
private readonly baseRef: string;
|
private readonly baseRef: string;
|
||||||
|
private readonly targetPath?: string;
|
||||||
|
|
||||||
constructor(input: {
|
constructor(input: {
|
||||||
worktreeRoot: string;
|
worktreeRoot: string;
|
||||||
baseRef: string;
|
baseRef: string;
|
||||||
|
targetPath?: string;
|
||||||
}) {
|
}) {
|
||||||
this.worktreeRoot = assertAbsolutePath(input.worktreeRoot, "worktreeRoot");
|
this.worktreeRoot = assertAbsolutePath(input.worktreeRoot, "worktreeRoot");
|
||||||
this.baseRef = assertNonEmptyString(input.baseRef, "baseRef");
|
this.baseRef = assertNonEmptyString(input.baseRef, "baseRef");
|
||||||
|
this.targetPath = normalizeWorktreeTargetPath(input.targetPath, "targetPath");
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveBaseWorkspacePath(sessionId: string): string {
|
resolveBaseWorkspacePath(sessionId: string): string {
|
||||||
@@ -378,6 +381,11 @@ export class SessionWorktreeManager {
|
|||||||
return resolve(this.worktreeRoot, scopedSession, "tasks", scopedTask);
|
return resolve(this.worktreeRoot, scopedSession, "tasks", scopedTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveWorkingDirectoryForWorktree(worktreePath: string): string {
|
||||||
|
const normalizedWorktreePath = assertAbsolutePath(worktreePath, "worktreePath");
|
||||||
|
return this.targetPath ? resolve(normalizedWorktreePath, this.targetPath) : normalizedWorktreePath;
|
||||||
|
}
|
||||||
|
|
||||||
private resolveBaseBranchName(sessionId: string): string {
|
private resolveBaseBranchName(sessionId: string): string {
|
||||||
const scoped = sanitizeSegment(sessionId, "session");
|
const scoped = sanitizeSegment(sessionId, "session");
|
||||||
return `ai-ops/${scoped}/base`;
|
return `ai-ops/${scoped}/base`;
|
||||||
@@ -399,16 +407,15 @@ export class SessionWorktreeManager {
|
|||||||
|
|
||||||
await mkdir(dirname(baseWorkspacePath), { recursive: true });
|
await mkdir(dirname(baseWorkspacePath), { recursive: true });
|
||||||
|
|
||||||
const alreadyExists = await pathExists(baseWorkspacePath);
|
if (!(await pathExists(baseWorkspacePath))) {
|
||||||
if (alreadyExists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]);
|
const repoRoot = await runGit(["-C", projectPath, "rev-parse", "--show-toplevel"]);
|
||||||
const branchName = this.resolveBaseBranchName(input.sessionId);
|
const branchName = this.resolveBaseBranchName(input.sessionId);
|
||||||
await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]);
|
await runGit(["-C", repoRoot, "worktree", "add", "-B", branchName, baseWorkspacePath, this.baseRef]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.ensureWorktreeTargetPath(baseWorkspacePath);
|
||||||
|
}
|
||||||
|
|
||||||
async ensureTaskWorktree(input: {
|
async ensureTaskWorktree(input: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -416,6 +423,7 @@ export class SessionWorktreeManager {
|
|||||||
existingWorktreePath?: string;
|
existingWorktreePath?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
taskWorktreePath: string;
|
taskWorktreePath: string;
|
||||||
|
taskWorkingDirectory: string;
|
||||||
}> {
|
}> {
|
||||||
const baseWorkspacePath = assertAbsolutePath(input.baseWorkspacePath, "baseWorkspacePath");
|
const baseWorkspacePath = assertAbsolutePath(input.baseWorkspacePath, "baseWorkspacePath");
|
||||||
const maybeExisting = input.existingWorktreePath?.trim();
|
const maybeExisting = input.existingWorktreePath?.trim();
|
||||||
@@ -451,8 +459,10 @@ export class SessionWorktreeManager {
|
|||||||
if (addResult.exitCode !== 0) {
|
if (addResult.exitCode !== 0) {
|
||||||
const attachedAfterFailure = await this.findWorktreePathForBranch(baseWorkspacePath, branchName);
|
const attachedAfterFailure = await this.findWorktreePathForBranch(baseWorkspacePath, branchName);
|
||||||
if (attachedAfterFailure === worktreePath && (await pathExists(worktreePath))) {
|
if (attachedAfterFailure === worktreePath && (await pathExists(worktreePath))) {
|
||||||
|
const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath);
|
||||||
return {
|
return {
|
||||||
taskWorktreePath: worktreePath,
|
taskWorktreePath: worktreePath,
|
||||||
|
taskWorkingDirectory,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -462,8 +472,10 @@ export class SessionWorktreeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const taskWorkingDirectory = await this.ensureWorktreeTargetPath(worktreePath);
|
||||||
return {
|
return {
|
||||||
taskWorktreePath: worktreePath,
|
taskWorktreePath: worktreePath,
|
||||||
|
taskWorkingDirectory,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,4 +792,69 @@ export class SessionWorktreeManager {
|
|||||||
}
|
}
|
||||||
return parseGitWorktreeRecords(result.stdout);
|
return parseGitWorktreeRecords(result.stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureWorktreeTargetPath(worktreePath: string): Promise<string> {
|
||||||
|
if (this.targetPath) {
|
||||||
|
await runGit(["-C", worktreePath, "sparse-checkout", "init", "--cone"]);
|
||||||
|
await runGit(["-C", worktreePath, "sparse-checkout", "set", this.targetPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workingDirectory = this.resolveWorkingDirectoryForWorktree(worktreePath);
|
||||||
|
let workingDirectoryStats;
|
||||||
|
try {
|
||||||
|
workingDirectoryStats = await stat(workingDirectory);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
if (this.targetPath) {
|
||||||
|
throw new Error(
|
||||||
|
`Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Worktree path "${workingDirectory}" does not exist.`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workingDirectoryStats.isDirectory()) {
|
||||||
|
if (this.targetPath) {
|
||||||
|
throw new Error(
|
||||||
|
`Configured worktree target path "${this.targetPath}" is not a directory in ref "${this.baseRef}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Worktree path "${workingDirectory}" is not a directory.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workingDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorktreeTargetPath(value: string | undefined, key: string): string | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashNormalized = trimmed.replace(/\\/g, "/");
|
||||||
|
if (isAbsolute(slashNormalized) || /^[a-zA-Z]:\//.test(slashNormalized)) {
|
||||||
|
throw new Error(`${key} must be a relative path within the repository worktree.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSegments = slashNormalized
|
||||||
|
.split("/")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter((segment) => segment.length > 0 && segment !== ".");
|
||||||
|
|
||||||
|
if (normalizedSegments.some((segment) => segment === "..")) {
|
||||||
|
throw new Error(`${key} must not contain ".." path segments.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedSegments.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSegments.join("/");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type ProviderRuntimeConfig = {
|
|||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
claudeModel?: string;
|
claudeModel?: string;
|
||||||
claudeCodePath?: string;
|
claudeCodePath?: string;
|
||||||
|
claudeMaxTurns: number;
|
||||||
claudeObservability: ClaudeObservabilityRuntimeConfig;
|
claudeObservability: ClaudeObservabilityRuntimeConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +137,8 @@ const DEFAULT_CLAUDE_OBSERVABILITY: ClaudeObservabilityRuntimeConfig = {
|
|||||||
debugLogPath: undefined,
|
debugLogPath: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_CLAUDE_MAX_TURNS = 2;
|
||||||
|
|
||||||
function readOptionalString(
|
function readOptionalString(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
key: string,
|
key: string,
|
||||||
@@ -401,6 +404,12 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Readonly<AppCo
|
|||||||
anthropicApiKey,
|
anthropicApiKey,
|
||||||
claudeModel: normalizeClaudeModel(readOptionalString(env, "CLAUDE_MODEL")),
|
claudeModel: normalizeClaudeModel(readOptionalString(env, "CLAUDE_MODEL")),
|
||||||
claudeCodePath: readOptionalString(env, "CLAUDE_CODE_PATH"),
|
claudeCodePath: readOptionalString(env, "CLAUDE_CODE_PATH"),
|
||||||
|
claudeMaxTurns: readIntegerWithBounds(
|
||||||
|
env,
|
||||||
|
"CLAUDE_MAX_TURNS",
|
||||||
|
DEFAULT_CLAUDE_MAX_TURNS,
|
||||||
|
{ min: 1 },
|
||||||
|
),
|
||||||
claudeObservability: {
|
claudeObservability: {
|
||||||
mode: parseClaudeObservabilityMode(
|
mode: parseClaudeObservabilityMode(
|
||||||
readStringWithFallback(
|
readStringWithFallback(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function requiredPrompt(argv: string[]): string {
|
|||||||
|
|
||||||
function buildOptions(config = getConfig()): Options {
|
function buildOptions(config = getConfig()): Options {
|
||||||
return {
|
return {
|
||||||
maxTurns: 1,
|
maxTurns: config.provider.claudeMaxTurns,
|
||||||
...(config.provider.claudeModel ? { model: config.provider.claudeModel } : {}),
|
...(config.provider.claudeModel ? { model: config.provider.claudeModel } : {}),
|
||||||
...(config.provider.claudeCodePath
|
...(config.provider.claudeCodePath
|
||||||
? { pathToClaudeCodeExecutable: config.provider.claudeCodePath }
|
? { pathToClaudeCodeExecutable: config.provider.claudeCodePath }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
parseShellValidationPolicy,
|
parseShellValidationPolicy,
|
||||||
parseToolClearancePolicy,
|
parseToolClearancePolicy,
|
||||||
|
type SecurityViolationHandling,
|
||||||
type ShellValidationPolicy,
|
type ShellValidationPolicy,
|
||||||
type ToolClearancePolicy,
|
type ToolClearancePolicy,
|
||||||
} from "./schemas.js";
|
} from "./schemas.js";
|
||||||
@@ -62,6 +63,10 @@ function normalizeToken(value: string): string {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLookupToken(value: string): string {
|
||||||
|
return normalizeToken(value).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
function hasPathTraversalSegment(token: string): boolean {
|
function hasPathTraversalSegment(token: string): boolean {
|
||||||
const normalized = token.replaceAll("\\", "/");
|
const normalized = token.replaceAll("\\", "/");
|
||||||
if (normalized === ".." || normalized.startsWith("../") || normalized.endsWith("/..")) {
|
if (normalized === ".." || normalized.startsWith("../") || normalized.endsWith("/..")) {
|
||||||
@@ -100,6 +105,18 @@ function toToolSet(values: readonly string[]): Set<string> {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toCaseInsensitiveLookup(values: readonly string[]): Map<string, string> {
|
||||||
|
const out = new Map<string, string>();
|
||||||
|
for (const value of values) {
|
||||||
|
const normalized = normalizeLookupToken(value);
|
||||||
|
if (!normalized || out.has(normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.set(normalized, value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function toNow(): string {
|
function toNow(): string {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -133,10 +150,14 @@ export class SecurityRulesEngine {
|
|||||||
private readonly blockedEnvAssignments: Set<string>;
|
private readonly blockedEnvAssignments: Set<string>;
|
||||||
private readonly worktreeRoot: string;
|
private readonly worktreeRoot: string;
|
||||||
private readonly protectedPaths: string[];
|
private readonly protectedPaths: string[];
|
||||||
|
private readonly violationHandling: SecurityViolationHandling;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
policy: ShellValidationPolicy,
|
policy: ShellValidationPolicy,
|
||||||
private readonly auditSink?: SecurityAuditSink,
|
private readonly auditSink?: SecurityAuditSink,
|
||||||
|
options?: {
|
||||||
|
violationHandling?: SecurityViolationHandling;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
this.policy = parseShellValidationPolicy(policy);
|
this.policy = parseShellValidationPolicy(policy);
|
||||||
this.allowedBinaries = toToolSet(this.policy.allowedBinaries);
|
this.allowedBinaries = toToolSet(this.policy.allowedBinaries);
|
||||||
@@ -144,6 +165,7 @@ export class SecurityRulesEngine {
|
|||||||
this.blockedEnvAssignments = toToolSet(this.policy.blockedEnvAssignments);
|
this.blockedEnvAssignments = toToolSet(this.policy.blockedEnvAssignments);
|
||||||
this.worktreeRoot = resolve(this.policy.worktreeRoot);
|
this.worktreeRoot = resolve(this.policy.worktreeRoot);
|
||||||
this.protectedPaths = this.policy.protectedPaths.map((path) => resolve(path));
|
this.protectedPaths = this.policy.protectedPaths.map((path) => resolve(path));
|
||||||
|
this.violationHandling = options?.violationHandling ?? "hard_abort";
|
||||||
}
|
}
|
||||||
|
|
||||||
getPolicy(): ShellValidationPolicy {
|
getPolicy(): ShellValidationPolicy {
|
||||||
@@ -212,6 +234,15 @@ export class SecurityRulesEngine {
|
|||||||
code: error.code,
|
code: error.code,
|
||||||
details: error.details,
|
details: error.details,
|
||||||
});
|
});
|
||||||
|
if (this.violationHandling === "dangerous_warn_only") {
|
||||||
|
return {
|
||||||
|
cwd: resolvedCwd,
|
||||||
|
parsed: {
|
||||||
|
commandCount: 0,
|
||||||
|
commands: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +263,11 @@ export class SecurityRulesEngine {
|
|||||||
};
|
};
|
||||||
}): void {
|
}): void {
|
||||||
const policy = parseToolClearancePolicy(input.toolClearance);
|
const policy = parseToolClearancePolicy(input.toolClearance);
|
||||||
|
const normalizedTool = normalizeLookupToken(input.tool);
|
||||||
|
const banlistLookup = toCaseInsensitiveLookup(policy.banlist);
|
||||||
|
const allowlistLookup = toCaseInsensitiveLookup(policy.allowlist);
|
||||||
|
|
||||||
if (policy.banlist.includes(input.tool)) {
|
if (banlistLookup.has(normalizedTool)) {
|
||||||
this.emit({
|
this.emit({
|
||||||
...toAuditContext(input.context),
|
...toAuditContext(input.context),
|
||||||
type: "tool.invocation_blocked",
|
type: "tool.invocation_blocked",
|
||||||
@@ -252,7 +286,7 @@ export class SecurityRulesEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policy.allowlist.length > 0 && !policy.allowlist.includes(input.tool)) {
|
if (policy.allowlist.length > 0 && !allowlistLookup.has(normalizedTool)) {
|
||||||
this.emit({
|
this.emit({
|
||||||
...toAuditContext(input.context),
|
...toAuditContext(input.context),
|
||||||
type: "tool.invocation_blocked",
|
type: "tool.invocation_blocked",
|
||||||
@@ -280,13 +314,15 @@ export class SecurityRulesEngine {
|
|||||||
|
|
||||||
filterAllowedTools(tools: string[], toolClearance: ToolClearancePolicy): string[] {
|
filterAllowedTools(tools: string[], toolClearance: ToolClearancePolicy): string[] {
|
||||||
const policy = parseToolClearancePolicy(toolClearance);
|
const policy = parseToolClearancePolicy(toolClearance);
|
||||||
|
const allowlistLookup = toCaseInsensitiveLookup(policy.allowlist);
|
||||||
|
const banlistLookup = toCaseInsensitiveLookup(policy.banlist);
|
||||||
|
|
||||||
const allowedByAllowlist =
|
const allowedByAllowlist =
|
||||||
policy.allowlist.length === 0
|
policy.allowlist.length === 0
|
||||||
? tools
|
? tools
|
||||||
: tools.filter((tool) => policy.allowlist.includes(tool));
|
: tools.filter((tool) => allowlistLookup.has(normalizeLookupToken(tool)));
|
||||||
|
|
||||||
return allowedByAllowlist.filter((tool) => !policy.banlist.includes(tool));
|
return allowedByAllowlist.filter((tool) => !banlistLookup.has(normalizeLookupToken(tool)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertCwdBoundary(cwd: string): void {
|
private assertCwdBoundary(cwd: string): void {
|
||||||
|
|||||||
@@ -157,11 +157,15 @@ export function parseParsedShellScript(input: unknown): ParsedShellScript {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecurityViolationHandling = "hard_abort" | "validation_fail";
|
export type SecurityViolationHandling =
|
||||||
|
| "hard_abort"
|
||||||
|
| "validation_fail"
|
||||||
|
| "dangerous_warn_only";
|
||||||
|
|
||||||
export const securityViolationHandlingSchema = z.union([
|
export const securityViolationHandlingSchema = z.union([
|
||||||
z.literal("hard_abort"),
|
z.literal("hard_abort"),
|
||||||
z.literal("validation_fail"),
|
z.literal("validation_fail"),
|
||||||
|
z.literal("dangerous_warn_only"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function parseSecurityViolationHandling(input: unknown): SecurityViolationHandling {
|
export function parseSecurityViolationHandling(input: unknown): SecurityViolationHandling {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { loadConfig, type AppConfig } from "../config.js";
|
import { loadConfig, type AppConfig } from "../config.js";
|
||||||
|
import type { SecurityViolationHandling } from "../security/index.js";
|
||||||
import { parseEnvFile, writeEnvFileUpdates } from "./env-store.js";
|
import { parseEnvFile, writeEnvFileUpdates } from "./env-store.js";
|
||||||
|
|
||||||
export type RuntimeNotificationSettings = {
|
export type RuntimeNotificationSettings = {
|
||||||
@@ -9,7 +10,7 @@ export type RuntimeNotificationSettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SecurityPolicySettings = {
|
export type SecurityPolicySettings = {
|
||||||
violationMode: "hard_abort" | "validation_fail";
|
violationMode: SecurityViolationHandling;
|
||||||
allowedBinaries: string[];
|
allowedBinaries: string[];
|
||||||
commandTimeoutMs: number;
|
commandTimeoutMs: number;
|
||||||
inheritedEnv: string[];
|
inheritedEnv: string[];
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { isDomainEventType, type DomainEventEmission } from "../agents/domain-events.js";
|
import { isDomainEventType, type DomainEventEmission } from "../agents/domain-events.js";
|
||||||
import type { ActorExecutionInput, ActorExecutionResult, ActorExecutor } from "../agents/pipeline.js";
|
import type { ActorExecutionInput, ActorExecutionResult, ActorExecutor } from "../agents/pipeline.js";
|
||||||
import { isRecord, type JsonObject, type JsonValue } from "../agents/types.js";
|
import { isRecord, type JsonObject, type JsonValue } from "../agents/types.js";
|
||||||
import { createSessionContext, type SessionContext } from "../examples/session-context.js";
|
|
||||||
import { ClaudeObservabilityLogger } from "./claude-observability.js";
|
import { ClaudeObservabilityLogger } from "./claude-observability.js";
|
||||||
|
|
||||||
export type RunProvider = "codex" | "claude";
|
export type RunProvider = "codex" | "claude";
|
||||||
@@ -17,7 +16,7 @@ export type RunProvider = "codex" | "claude";
|
|||||||
export type ProviderRunRuntime = {
|
export type ProviderRunRuntime = {
|
||||||
provider: RunProvider;
|
provider: RunProvider;
|
||||||
config: Readonly<AppConfig>;
|
config: Readonly<AppConfig>;
|
||||||
sessionContext: SessionContext;
|
sharedEnv: Record<string, string>;
|
||||||
claudeObservability: ClaudeObservabilityLogger;
|
claudeObservability: ClaudeObservabilityLogger;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -30,6 +29,16 @@ type ProviderUsage = {
|
|||||||
costUsd?: number;
|
costUsd?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function sanitizeEnv(input: Record<string, string | undefined>): Record<string, string> {
|
||||||
|
const output: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(input)) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
output[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTOR_RESPONSE_SCHEMA = {
|
const ACTOR_RESPONSE_SCHEMA = {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: true,
|
additionalProperties: true,
|
||||||
@@ -74,8 +83,6 @@ const CLAUDE_OUTPUT_FORMAT = {
|
|||||||
schema: ACTOR_RESPONSE_SCHEMA,
|
schema: ACTOR_RESPONSE_SCHEMA,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const CLAUDE_PROVIDER_MAX_TURNS = 2;
|
|
||||||
|
|
||||||
function toErrorMessage(error: unknown): string {
|
function toErrorMessage(error: unknown): string {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return error.message;
|
return error.message;
|
||||||
@@ -83,6 +90,23 @@ function toErrorMessage(error: unknown): string {
|
|||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveProviderWorkingDirectory(actorInput: ActorExecutionInput): string {
|
||||||
|
return actorInput.executionContext.security.worktreePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildProviderRuntimeEnv(input: {
|
||||||
|
runtime: ProviderRunRuntime;
|
||||||
|
actorInput: ActorExecutionInput;
|
||||||
|
includeClaudeAuth?: boolean;
|
||||||
|
}): Record<string, string> {
|
||||||
|
const workingDirectory = resolveProviderWorkingDirectory(input.actorInput);
|
||||||
|
return sanitizeEnv({
|
||||||
|
...input.runtime.sharedEnv,
|
||||||
|
...(input.includeClaudeAuth ? buildClaudeAuthEnv(input.runtime.config.provider) : {}),
|
||||||
|
AGENT_WORKTREE_PATH: workingDirectory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toJsonValue(value: unknown): JsonValue {
|
function toJsonValue(value: unknown): JsonValue {
|
||||||
return JSON.parse(JSON.stringify(value)) as JsonValue;
|
return JSON.parse(JSON.stringify(value)) as JsonValue;
|
||||||
}
|
}
|
||||||
@@ -367,6 +391,7 @@ async function runCodexActor(input: {
|
|||||||
const prompt = buildActorPrompt(actorInput);
|
const prompt = buildActorPrompt(actorInput);
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const apiKey = resolveOpenAiApiKey(runtime.config.provider);
|
const apiKey = resolveOpenAiApiKey(runtime.config.provider);
|
||||||
|
const workingDirectory = resolveProviderWorkingDirectory(actorInput);
|
||||||
|
|
||||||
const codex = new Codex({
|
const codex = new Codex({
|
||||||
...(apiKey ? { apiKey } : {}),
|
...(apiKey ? { apiKey } : {}),
|
||||||
@@ -376,20 +401,21 @@ async function runCodexActor(input: {
|
|||||||
...(actorInput.mcp.resolvedConfig.codexConfig
|
...(actorInput.mcp.resolvedConfig.codexConfig
|
||||||
? { config: actorInput.mcp.resolvedConfig.codexConfig }
|
? { config: actorInput.mcp.resolvedConfig.codexConfig }
|
||||||
: {}),
|
: {}),
|
||||||
env: runtime.sessionContext.runtimeInjection.env,
|
env: buildProviderRuntimeEnv({
|
||||||
|
runtime,
|
||||||
|
actorInput,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const thread = codex.startThread({
|
const thread = codex.startThread({
|
||||||
workingDirectory: runtime.sessionContext.runtimeInjection.workingDirectory,
|
workingDirectory,
|
||||||
skipGitRepoCheck: runtime.config.provider.codexSkipGitCheck,
|
skipGitRepoCheck: runtime.config.provider.codexSkipGitCheck,
|
||||||
});
|
});
|
||||||
|
|
||||||
const turn = await runtime.sessionContext.runInSession(() =>
|
const turn = await thread.run(prompt, {
|
||||||
thread.run(prompt, {
|
|
||||||
signal: actorInput.signal,
|
signal: actorInput.signal,
|
||||||
outputSchema: ACTOR_RESPONSE_SCHEMA,
|
outputSchema: ACTOR_RESPONSE_SCHEMA,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const usage: ProviderUsage = {
|
const usage: ProviderUsage = {
|
||||||
...(turn.usage
|
...(turn.usage
|
||||||
@@ -457,6 +483,7 @@ function buildClaudeOptions(input: {
|
|||||||
actorInput: ActorExecutionInput;
|
actorInput: ActorExecutionInput;
|
||||||
}): Options {
|
}): Options {
|
||||||
const { runtime, actorInput } = input;
|
const { runtime, actorInput } = input;
|
||||||
|
const workingDirectory = resolveProviderWorkingDirectory(actorInput);
|
||||||
|
|
||||||
const authOptionOverrides = runtime.config.provider.anthropicOauthToken
|
const authOptionOverrides = runtime.config.provider.anthropicOauthToken
|
||||||
? { authToken: runtime.config.provider.anthropicOauthToken }
|
? { authToken: runtime.config.provider.anthropicOauthToken }
|
||||||
@@ -465,14 +492,15 @@ function buildClaudeOptions(input: {
|
|||||||
return token ? { apiKey: token } : {};
|
return token ? { apiKey: token } : {};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const runtimeEnv = {
|
const runtimeEnv = buildProviderRuntimeEnv({
|
||||||
...runtime.sessionContext.runtimeInjection.env,
|
runtime,
|
||||||
...buildClaudeAuthEnv(runtime.config.provider),
|
actorInput,
|
||||||
};
|
includeClaudeAuth: true,
|
||||||
|
});
|
||||||
const traceContext = toClaudeTraceContext(actorInput);
|
const traceContext = toClaudeTraceContext(actorInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
maxTurns: CLAUDE_PROVIDER_MAX_TURNS,
|
maxTurns: runtime.config.provider.claudeMaxTurns,
|
||||||
...(runtime.config.provider.claudeModel
|
...(runtime.config.provider.claudeModel
|
||||||
? { model: runtime.config.provider.claudeModel }
|
? { model: runtime.config.provider.claudeModel }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -484,7 +512,7 @@ function buildClaudeOptions(input: {
|
|||||||
? { mcpServers: actorInput.mcp.resolvedConfig.claudeMcpServers as Options["mcpServers"] }
|
? { mcpServers: actorInput.mcp.resolvedConfig.claudeMcpServers as Options["mcpServers"] }
|
||||||
: {}),
|
: {}),
|
||||||
canUseTool: actorInput.mcp.createClaudeCanUseTool(),
|
canUseTool: actorInput.mcp.createClaudeCanUseTool(),
|
||||||
cwd: runtime.sessionContext.runtimeInjection.workingDirectory,
|
cwd: workingDirectory,
|
||||||
env: runtimeEnv,
|
env: runtimeEnv,
|
||||||
...runtime.claudeObservability.toOptionOverrides({
|
...runtime.claudeObservability.toOptionOverrides({
|
||||||
context: traceContext,
|
context: traceContext,
|
||||||
@@ -507,8 +535,8 @@ async function runClaudeTurn(input: {
|
|||||||
context: traceContext,
|
context: traceContext,
|
||||||
data: {
|
data: {
|
||||||
...(options.model ? { model: options.model } : {}),
|
...(options.model ? { model: options.model } : {}),
|
||||||
maxTurns: options.maxTurns ?? CLAUDE_PROVIDER_MAX_TURNS,
|
maxTurns: options.maxTurns ?? input.runtime.config.provider.claudeMaxTurns,
|
||||||
cwd: input.runtime.sessionContext.runtimeInjection.workingDirectory,
|
...(typeof options.cwd === "string" ? { cwd: options.cwd } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,13 +633,11 @@ async function runClaudeActor(input: {
|
|||||||
actorInput: ActorExecutionInput;
|
actorInput: ActorExecutionInput;
|
||||||
}): Promise<ActorExecutionResult> {
|
}): Promise<ActorExecutionResult> {
|
||||||
const prompt = buildActorPrompt(input.actorInput);
|
const prompt = buildActorPrompt(input.actorInput);
|
||||||
const turn = await input.runtime.sessionContext.runInSession(() =>
|
const turn = await runClaudeTurn({
|
||||||
runClaudeTurn({
|
|
||||||
runtime: input.runtime,
|
runtime: input.runtime,
|
||||||
actorInput: input.actorInput,
|
actorInput: input.actorInput,
|
||||||
prompt,
|
prompt,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const parsed = parseActorExecutionResultFromModelOutput({
|
const parsed = parseActorExecutionResultFromModelOutput({
|
||||||
rawText: turn.text,
|
rawText: turn.text,
|
||||||
@@ -626,33 +652,21 @@ async function runClaudeActor(input: {
|
|||||||
|
|
||||||
export async function createProviderRunRuntime(input: {
|
export async function createProviderRunRuntime(input: {
|
||||||
provider: RunProvider;
|
provider: RunProvider;
|
||||||
initialPrompt: string;
|
|
||||||
config: Readonly<AppConfig>;
|
config: Readonly<AppConfig>;
|
||||||
projectPath: string;
|
|
||||||
observabilityRootPath?: string;
|
observabilityRootPath?: string;
|
||||||
|
baseEnv?: Record<string, string | undefined>;
|
||||||
}): Promise<ProviderRunRuntime> {
|
}): Promise<ProviderRunRuntime> {
|
||||||
const sessionContext = await createSessionContext(input.provider, {
|
|
||||||
prompt: input.initialPrompt,
|
|
||||||
config: input.config,
|
|
||||||
workspaceRoot: input.projectPath,
|
|
||||||
});
|
|
||||||
const claudeObservability = new ClaudeObservabilityLogger({
|
const claudeObservability = new ClaudeObservabilityLogger({
|
||||||
workspaceRoot: input.observabilityRootPath ?? input.projectPath,
|
workspaceRoot: input.observabilityRootPath ?? process.cwd(),
|
||||||
config: input.config.provider.claudeObservability,
|
config: input.config.provider.claudeObservability,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: input.provider,
|
provider: input.provider,
|
||||||
config: input.config,
|
config: input.config,
|
||||||
sessionContext,
|
sharedEnv: sanitizeEnv(input.baseEnv ?? process.env),
|
||||||
claudeObservability,
|
claudeObservability,
|
||||||
close: async () => {
|
close: async () => claudeObservability.close(),
|
||||||
try {
|
|
||||||
await sessionContext.close();
|
|
||||||
} finally {
|
|
||||||
await claudeObservability.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@
|
|||||||
<select id="cfg-security-mode">
|
<select id="cfg-security-mode">
|
||||||
<option value="hard_abort">hard_abort</option>
|
<option value="hard_abort">hard_abort</option>
|
||||||
<option value="validation_fail">validation_fail</option>
|
<option value="validation_fail">validation_fail</option>
|
||||||
|
<option value="dangerous_warn_only">dangerous_warn_only</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ export class UiRunService {
|
|||||||
worktreeManager: new SessionWorktreeManager({
|
worktreeManager: new SessionWorktreeManager({
|
||||||
worktreeRoot: paths.worktreeRoot,
|
worktreeRoot: paths.worktreeRoot,
|
||||||
baseRef: config.provisioning.gitWorktree.baseRef,
|
baseRef: config.provisioning.gitWorktree.baseRef,
|
||||||
|
targetPath: config.provisioning.gitWorktree.targetPath,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -485,10 +486,9 @@ export class UiRunService {
|
|||||||
if (executionMode === "provider") {
|
if (executionMode === "provider") {
|
||||||
providerRuntime = await createProviderRunRuntime({
|
providerRuntime = await createProviderRunRuntime({
|
||||||
provider,
|
provider,
|
||||||
initialPrompt: input.prompt,
|
|
||||||
config,
|
config,
|
||||||
projectPath: session?.baseWorkspacePath ?? this.workspaceRoot,
|
|
||||||
observabilityRootPath: this.workspaceRoot,
|
observabilityRootPath: this.workspaceRoot,
|
||||||
|
baseEnv: process.env,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ test("loads defaults and freezes config", () => {
|
|||||||
"session.failed",
|
"session.failed",
|
||||||
]);
|
]);
|
||||||
assert.equal(config.provider.openAiAuthMode, "auto");
|
assert.equal(config.provider.openAiAuthMode, "auto");
|
||||||
|
assert.equal(config.provider.claudeMaxTurns, 2);
|
||||||
assert.equal(config.provider.claudeObservability.mode, "off");
|
assert.equal(config.provider.claudeObservability.mode, "off");
|
||||||
assert.equal(config.provider.claudeObservability.verbosity, "summary");
|
assert.equal(config.provider.claudeObservability.verbosity, "summary");
|
||||||
assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/events/claude-trace.ndjson");
|
assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/events/claude-trace.ndjson");
|
||||||
@@ -55,6 +56,11 @@ test("validates security violation mode", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("loads dangerous_warn_only security violation mode", () => {
|
||||||
|
const config = loadConfig({ AGENT_SECURITY_VIOLATION_MODE: "dangerous_warn_only" });
|
||||||
|
assert.equal(config.security.violationHandling, "dangerous_warn_only");
|
||||||
|
});
|
||||||
|
|
||||||
test("validates runtime discord severity mode", () => {
|
test("validates runtime discord severity mode", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }),
|
() => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }),
|
||||||
@@ -69,6 +75,13 @@ test("validates claude observability mode", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validates CLAUDE_MAX_TURNS bounds", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => loadConfig({ CLAUDE_MAX_TURNS: "0" }),
|
||||||
|
/CLAUDE_MAX_TURNS must be an integer >= 1/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("validates claude observability verbosity", () => {
|
test("validates claude observability verbosity", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => loadConfig({ CLAUDE_OBSERVABILITY_VERBOSITY: "verbose" }),
|
() => loadConfig({ CLAUDE_OBSERVABILITY_VERBOSITY: "verbose" }),
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ test("injects resolved mcp/helpers and enforces Claude tool gate in actor execut
|
|||||||
);
|
);
|
||||||
assert.deepEqual(allow, {
|
assert.deepEqual(allow, {
|
||||||
behavior: "allow",
|
behavior: "allow",
|
||||||
|
updatedInput: {},
|
||||||
toolUseID: "allow-1",
|
toolUseID: "allow-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -997,6 +998,7 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
|||||||
});
|
});
|
||||||
assert.deepEqual(allow, {
|
assert.deepEqual(allow, {
|
||||||
behavior: "allow",
|
behavior: "allow",
|
||||||
|
updatedInput: {},
|
||||||
toolUseID: "allow-bash",
|
toolUseID: "allow-bash",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1020,6 +1022,88 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
|||||||
assert.equal(result.status, "success");
|
assert.equal(result.status, "success");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("dangerous_warn_only allows tool use outside persona allowlist", async () => {
|
||||||
|
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||||
|
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||||
|
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
schemaVersion: "1",
|
||||||
|
topologies: ["sequential"],
|
||||||
|
personas: [
|
||||||
|
{
|
||||||
|
id: "reader",
|
||||||
|
displayName: "Reader",
|
||||||
|
systemPromptTemplate: "Reader",
|
||||||
|
toolClearance: {
|
||||||
|
allowlist: ["read_file"],
|
||||||
|
banlist: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
topologyConstraints: {
|
||||||
|
maxDepth: 2,
|
||||||
|
maxRetries: 0,
|
||||||
|
},
|
||||||
|
pipeline: {
|
||||||
|
entryNodeId: "warn-node",
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "warn-node",
|
||||||
|
actorId: "warn_actor",
|
||||||
|
personaId: "reader",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const engine = new SchemaDrivenExecutionEngine({
|
||||||
|
manifest,
|
||||||
|
settings: {
|
||||||
|
workspaceRoot,
|
||||||
|
stateRoot,
|
||||||
|
projectContextPath,
|
||||||
|
maxChildren: 1,
|
||||||
|
maxDepth: 2,
|
||||||
|
maxRetries: 0,
|
||||||
|
securityViolationHandling: "dangerous_warn_only",
|
||||||
|
runtimeContext: {},
|
||||||
|
},
|
||||||
|
actorExecutors: {
|
||||||
|
warn_actor: async (input) => {
|
||||||
|
const canUseTool = input.mcp.createClaudeCanUseTool();
|
||||||
|
const allow = await canUseTool("Bash", {}, {
|
||||||
|
signal: new AbortController().signal,
|
||||||
|
toolUseID: "allow-bash-warn",
|
||||||
|
});
|
||||||
|
assert.deepEqual(allow, {
|
||||||
|
behavior: "allow",
|
||||||
|
updatedInput: {},
|
||||||
|
toolUseID: "allow-bash-warn",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
payload: {
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await engine.runSession({
|
||||||
|
sessionId: "session-dangerous-warn-only",
|
||||||
|
initialPayload: {
|
||||||
|
task: "verify warn-only bypass",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.status, "success");
|
||||||
|
});
|
||||||
|
|
||||||
test("hard-aborts pipeline on security violations by default", async () => {
|
test("hard-aborts pipeline on security violations by default", async () => {
|
||||||
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
|||||||
ANTHROPIC_API_KEY: "legacy-api-key",
|
ANTHROPIC_API_KEY: "legacy-api-key",
|
||||||
CLAUDE_MODEL: "claude-sonnet-4-6",
|
CLAUDE_MODEL: "claude-sonnet-4-6",
|
||||||
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
|
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
|
||||||
|
CLAUDE_MAX_TURNS: "5",
|
||||||
});
|
});
|
||||||
|
|
||||||
let closed = false;
|
let closed = false;
|
||||||
@@ -229,6 +230,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
|||||||
assert.equal(queryInput?.prompt, "augmented prompt");
|
assert.equal(queryInput?.prompt, "augmented prompt");
|
||||||
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
|
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
|
||||||
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
|
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
|
||||||
|
assert.equal(queryInput?.options?.maxTurns, 5);
|
||||||
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
|
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
|
||||||
assert.equal(queryInput?.options?.authToken, "oauth-token");
|
assert.equal(queryInput?.options?.authToken, "oauth-token");
|
||||||
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
|
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { parseActorExecutionResultFromModelOutput } from "../src/ui/provider-executor.js";
|
import { mkdtemp } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { loadConfig } from "../src/config.js";
|
||||||
|
import type { ActorExecutionInput } from "../src/agents/pipeline.js";
|
||||||
|
import {
|
||||||
|
buildProviderRuntimeEnv,
|
||||||
|
createProviderRunRuntime,
|
||||||
|
parseActorExecutionResultFromModelOutput,
|
||||||
|
resolveProviderWorkingDirectory,
|
||||||
|
type ProviderRunRuntime,
|
||||||
|
} from "../src/ui/provider-executor.js";
|
||||||
|
|
||||||
test("parseActorExecutionResultFromModelOutput parses strict JSON payload", () => {
|
test("parseActorExecutionResultFromModelOutput parses strict JSON payload", () => {
|
||||||
const parsed = parseActorExecutionResultFromModelOutput({
|
const parsed = parseActorExecutionResultFromModelOutput({
|
||||||
@@ -64,3 +75,71 @@ test("parseActorExecutionResultFromModelOutput falls back when response is not J
|
|||||||
assert.equal(parsed.status, "success");
|
assert.equal(parsed.status, "success");
|
||||||
assert.equal(parsed.payload?.assistantResponse, "Implemented update successfully.");
|
assert.equal(parsed.payload?.assistantResponse, "Implemented update successfully.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resolveProviderWorkingDirectory reads cwd from actor execution context", () => {
|
||||||
|
const actorInput = {
|
||||||
|
executionContext: {
|
||||||
|
security: {
|
||||||
|
worktreePath: "/tmp/session/tasks/product-intake",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as ActorExecutionInput;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
resolveProviderWorkingDirectory(actorInput),
|
||||||
|
"/tmp/session/tasks/product-intake",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildProviderRuntimeEnv scopes AGENT_WORKTREE_PATH to actor worktree and filters undefined auth", () => {
|
||||||
|
const config = loadConfig({
|
||||||
|
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
|
||||||
|
});
|
||||||
|
const runtime = {
|
||||||
|
provider: "claude",
|
||||||
|
config,
|
||||||
|
sharedEnv: {
|
||||||
|
PATH: "/usr/bin",
|
||||||
|
KEEP_ME: "1",
|
||||||
|
},
|
||||||
|
claudeObservability: {} as ProviderRunRuntime["claudeObservability"],
|
||||||
|
close: async () => {},
|
||||||
|
} satisfies ProviderRunRuntime;
|
||||||
|
const actorInput = {
|
||||||
|
executionContext: {
|
||||||
|
security: {
|
||||||
|
worktreePath: "/tmp/session/tasks/product-intake",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as ActorExecutionInput;
|
||||||
|
|
||||||
|
const env = buildProviderRuntimeEnv({
|
||||||
|
runtime,
|
||||||
|
actorInput,
|
||||||
|
includeClaudeAuth: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(env.AGENT_WORKTREE_PATH, "/tmp/session/tasks/product-intake");
|
||||||
|
assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token");
|
||||||
|
assert.equal("ANTHROPIC_API_KEY" in env, false);
|
||||||
|
assert.equal(env.KEEP_ME, "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createProviderRunRuntime does not require session context provisioning", async () => {
|
||||||
|
const observabilityRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-provider-runtime-"));
|
||||||
|
const runtime = await createProviderRunRuntime({
|
||||||
|
provider: "claude",
|
||||||
|
config: loadConfig({}),
|
||||||
|
observabilityRootPath: observabilityRoot,
|
||||||
|
baseEnv: {
|
||||||
|
PATH: "/usr/bin",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.equal(runtime.provider, "claude");
|
||||||
|
assert.equal(runtime.sharedEnv.PATH, "/usr/bin");
|
||||||
|
} finally {
|
||||||
|
await runtime.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -111,6 +111,42 @@ test("rules engine enforces binary allowlist, tool policy, and path boundaries",
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rules engine dangerous_warn_only logs but does not block violating shell commands", async () => {
|
||||||
|
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-worktree-"));
|
||||||
|
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-state-"));
|
||||||
|
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||||
|
|
||||||
|
const rules = new SecurityRulesEngine(
|
||||||
|
{
|
||||||
|
allowedBinaries: ["git"],
|
||||||
|
worktreeRoot,
|
||||||
|
protectedPaths: [stateRoot, projectContextPath],
|
||||||
|
requireCwdWithinWorktree: true,
|
||||||
|
rejectRelativePathTraversal: true,
|
||||||
|
enforcePathBoundaryOnArguments: true,
|
||||||
|
allowedEnvAssignments: [],
|
||||||
|
blockedEnvAssignments: [],
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
violationHandling: "dangerous_warn_only",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const validated = await rules.validateShellCommand({
|
||||||
|
command: "unauthorized_bin --version",
|
||||||
|
cwd: worktreeRoot,
|
||||||
|
toolClearance: {
|
||||||
|
allowlist: ["git"],
|
||||||
|
banlist: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(validated.cwd, worktreeRoot);
|
||||||
|
assert.equal(validated.parsed.commandCount, 0);
|
||||||
|
assert.deepEqual(validated.parsed.commands, []);
|
||||||
|
});
|
||||||
|
|
||||||
test("secure executor runs with explicit env policy", async () => {
|
test("secure executor runs with explicit env policy", async () => {
|
||||||
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-exec-"));
|
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-exec-"));
|
||||||
|
|
||||||
@@ -193,3 +229,47 @@ test("rules engine carries session context in tool audit events", () => {
|
|||||||
assert.equal(allowedEvent.nodeId, "node-ctx");
|
assert.equal(allowedEvent.nodeId, "node-ctx");
|
||||||
assert.equal(allowedEvent.attempt, 2);
|
assert.equal(allowedEvent.attempt, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rules engine applies tool clearance matching case-insensitively", () => {
|
||||||
|
const rules = new SecurityRulesEngine({
|
||||||
|
allowedBinaries: ["git"],
|
||||||
|
worktreeRoot: "/tmp",
|
||||||
|
protectedPaths: [],
|
||||||
|
requireCwdWithinWorktree: true,
|
||||||
|
rejectRelativePathTraversal: true,
|
||||||
|
enforcePathBoundaryOnArguments: true,
|
||||||
|
allowedEnvAssignments: [],
|
||||||
|
blockedEnvAssignments: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
rules.assertToolInvocationAllowed({
|
||||||
|
tool: "Bash",
|
||||||
|
toolClearance: {
|
||||||
|
allowlist: ["bash", "glob"],
|
||||||
|
banlist: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
rules.assertToolInvocationAllowed({
|
||||||
|
tool: "Glob",
|
||||||
|
toolClearance: {
|
||||||
|
allowlist: ["bash", "glob"],
|
||||||
|
banlist: ["GLOB"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
(error: unknown) =>
|
||||||
|
error instanceof SecurityViolationError && error.code === "TOOL_BANNED",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
rules.filterAllowedTools(["Bash", "Glob", "Read"], {
|
||||||
|
allowlist: ["bash", "glob"],
|
||||||
|
banlist: ["gLoB"],
|
||||||
|
}),
|
||||||
|
["Bash"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -228,3 +228,60 @@ test("session worktree manager recreates a task worktree after stale metadata pr
|
|||||||
const stats = await stat(recreatedTaskWorktreePath);
|
const stats = await stat(recreatedTaskWorktreePath);
|
||||||
assert.equal(stats.isDirectory(), true);
|
assert.equal(stats.isDirectory(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("session worktree manager applies target path sparse checkout and task working directory", async () => {
|
||||||
|
const root = await mkdtemp(resolve(tmpdir(), "ai-ops-session-worktree-target-"));
|
||||||
|
const projectPath = resolve(root, "project");
|
||||||
|
const worktreeRoot = resolve(root, "worktrees");
|
||||||
|
|
||||||
|
await mkdir(resolve(projectPath, "app", "src"), { recursive: true });
|
||||||
|
await mkdir(resolve(projectPath, "infra"), { recursive: true });
|
||||||
|
await git(["init", projectPath]);
|
||||||
|
await git(["-C", projectPath, "config", "user.name", "AI Ops"]);
|
||||||
|
await git(["-C", projectPath, "config", "user.email", "ai-ops@example.local"]);
|
||||||
|
await writeFile(resolve(projectPath, "app", "src", "index.ts"), "export const app = true;\n", "utf8");
|
||||||
|
await writeFile(resolve(projectPath, "infra", "notes.txt"), "infra\n", "utf8");
|
||||||
|
await git(["-C", projectPath, "add", "."]);
|
||||||
|
await git(["-C", projectPath, "commit", "-m", "initial commit"]);
|
||||||
|
|
||||||
|
const manager = new SessionWorktreeManager({
|
||||||
|
worktreeRoot,
|
||||||
|
baseRef: "HEAD",
|
||||||
|
targetPath: "app",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionId = "session-target-1";
|
||||||
|
const baseWorkspacePath = manager.resolveBaseWorkspacePath(sessionId);
|
||||||
|
await manager.initializeSessionBaseWorkspace({
|
||||||
|
sessionId,
|
||||||
|
projectPath,
|
||||||
|
baseWorkspacePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseWorkingDirectory = manager.resolveWorkingDirectoryForWorktree(baseWorkspacePath);
|
||||||
|
assert.equal(baseWorkingDirectory, resolve(baseWorkspacePath, "app"));
|
||||||
|
const baseWorkingStats = await stat(baseWorkingDirectory);
|
||||||
|
assert.equal(baseWorkingStats.isDirectory(), true);
|
||||||
|
await assert.rejects(() => stat(resolve(baseWorkspacePath, "infra")), {
|
||||||
|
code: "ENOENT",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ensured = await manager.ensureTaskWorktree({
|
||||||
|
sessionId,
|
||||||
|
taskId: "task-target-1",
|
||||||
|
baseWorkspacePath,
|
||||||
|
});
|
||||||
|
assert.equal(ensured.taskWorkingDirectory, resolve(ensured.taskWorktreePath, "app"));
|
||||||
|
|
||||||
|
await writeFile(resolve(ensured.taskWorkingDirectory, "src", "feature.ts"), "export const feature = true;\n", "utf8");
|
||||||
|
|
||||||
|
const mergeOutcome = await manager.mergeTaskIntoBase({
|
||||||
|
taskId: "task-target-1",
|
||||||
|
baseWorkspacePath,
|
||||||
|
taskWorktreePath: ensured.taskWorktreePath,
|
||||||
|
});
|
||||||
|
assert.equal(mergeOutcome.kind, "success");
|
||||||
|
|
||||||
|
const merged = await readFile(resolve(baseWorkingDirectory, "src", "feature.ts"), "utf8");
|
||||||
|
assert.equal(merged, "export const feature = true;\n");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user