feat: harden orchestrator execution runtime
This commit is contained in:
@@ -61,15 +61,24 @@ function eventFor(state, type, data = null) {
|
||||
});
|
||||
}
|
||||
|
||||
function validateNode(state) {
|
||||
function validateNode(state, { executionEnabled = false } = {}) {
|
||||
const spec = normalizeRunSpec(state.spec);
|
||||
if (spec.workflow.name !== CODE_RUN_SHADOW_WORKFLOW) {
|
||||
const error = new Error(`Unsupported workflow: ${spec.workflow.name}`);
|
||||
error.code = 'WORKFLOW_NOT_SUPPORTED';
|
||||
throw error;
|
||||
}
|
||||
if (spec.policy.executionMode !== 'observe-only' || spec.policy.sideEffectsAllowed !== false) {
|
||||
const error = new Error('Phase 2 LangGraph service accepts observe-only runs');
|
||||
const observeOnly = spec.policy.executionMode === 'observe-only'
|
||||
&& spec.policy.sideEffectsAllowed === false;
|
||||
const active = spec.policy.executionMode === 'active'
|
||||
&& spec.policy.sideEffectsAllowed === true
|
||||
&& executionEnabled;
|
||||
if (!observeOnly && !active) {
|
||||
const error = new Error(
|
||||
executionEnabled
|
||||
? 'LangGraph run policy must be observe-only or explicitly active'
|
||||
: 'LangGraph execution is disabled; only observe-only runs are accepted',
|
||||
);
|
||||
error.code = 'WORKFLOW_OBSERVE_ONLY_REQUIRED';
|
||||
throw error;
|
||||
}
|
||||
@@ -79,7 +88,7 @@ function validateNode(state) {
|
||||
phase: 'validated',
|
||||
events: [eventFor(state, 'workflow_validated', {
|
||||
workflow: spec.workflow,
|
||||
executionMode: 'observe-only',
|
||||
executionMode: spec.policy.executionMode,
|
||||
})],
|
||||
};
|
||||
}
|
||||
@@ -88,10 +97,12 @@ async function planNode(state, executorGateway) {
|
||||
const taskType = String(state.spec.input?.taskType ?? '').trim() || 'code-change';
|
||||
const instruction = String(state.spec.input?.instruction ?? '');
|
||||
const executorIdentity = executorIdentityForRun(state.spec.runId);
|
||||
const active = state.spec.policy.executionMode === 'active';
|
||||
const executor = String(state.spec.input?.executor ?? 'goosed').trim().toLowerCase();
|
||||
const executorJob = await executorGateway.createJob({
|
||||
jobId: executorIdentity.jobId,
|
||||
idempotencyKey: executorIdentity.idempotencyKey,
|
||||
executor: 'goosed',
|
||||
executor,
|
||||
task: {
|
||||
type: taskType,
|
||||
instruction,
|
||||
@@ -100,17 +111,18 @@ async function planNode(state, executorGateway) {
|
||||
},
|
||||
subject: state.spec.subject,
|
||||
authorization: {
|
||||
executionAllowed: false,
|
||||
executionAllowed: active,
|
||||
actorId: state.spec.subject?.userId ?? null,
|
||||
},
|
||||
policy: {
|
||||
sideEffectsAllowed: false,
|
||||
networkAllowed: false,
|
||||
sideEffectsAllowed: active && state.spec.policy.sideEffectsAllowed === true,
|
||||
networkAllowed: active && state.spec.policy.networkAllowed === true,
|
||||
},
|
||||
controls: {
|
||||
timeoutMs: state.spec.limits?.timeoutMs,
|
||||
cancellationAllowed: true,
|
||||
fallbackExecutor: 'aider',
|
||||
maxAttempts: state.spec.limits?.maxAttempts,
|
||||
},
|
||||
metadata: {
|
||||
source: 'langgraph-shadow-plan',
|
||||
@@ -119,7 +131,7 @@ async function planNode(state, executorGateway) {
|
||||
});
|
||||
const plan = {
|
||||
taskType,
|
||||
executorAdapter: 'native-agent-run',
|
||||
executorAdapter: active ? 'executor-gateway' : 'native-agent-run',
|
||||
executorJob: {
|
||||
kind: 'executor-job',
|
||||
id: executorJob.job.jobId,
|
||||
@@ -148,27 +160,36 @@ async function planNode(state, executorGateway) {
|
||||
}
|
||||
|
||||
function finalizeNode(state) {
|
||||
const queued = state.plan.executorJob.status === 'queued';
|
||||
const result = {
|
||||
observed: true,
|
||||
observed: !queued,
|
||||
executed: false,
|
||||
executorAdapter: state.plan.executorAdapter,
|
||||
taskType: state.plan.taskType,
|
||||
};
|
||||
return {
|
||||
status: 'succeeded',
|
||||
phase: 'completed',
|
||||
status: queued ? 'waiting' : 'succeeded',
|
||||
phase: queued ? 'executor_queued' : 'completed',
|
||||
result,
|
||||
events: [eventFor(state, 'workflow_completed', result)],
|
||||
events: [eventFor(
|
||||
state,
|
||||
queued ? 'workflow_executor_queued' : 'workflow_completed',
|
||||
result,
|
||||
)],
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeRunShadowGraph({ checkpointer, executorGateway } = {}) {
|
||||
export function createCodeRunShadowGraph({
|
||||
checkpointer,
|
||||
executorGateway,
|
||||
executionEnabled = false,
|
||||
} = {}) {
|
||||
if (!checkpointer) throw new Error('Code run shadow graph requires a checkpointer');
|
||||
if (!executorGateway?.createJob) {
|
||||
throw new Error('Code run shadow graph requires an Executor Gateway');
|
||||
}
|
||||
return new StateGraph(ShadowState)
|
||||
.addNode('validate_run', validateNode)
|
||||
.addNode('validate_run', (state) => validateNode(state, { executionEnabled }))
|
||||
.addNode('build_plan', (state) => planNode(state, executorGateway))
|
||||
.addNode('finalize_run', finalizeNode)
|
||||
.addEdge(START, 'validate_run')
|
||||
|
||||
Reference in New Issue
Block a user