import { dirname, isAbsolute, resolve } from 'node:path'; import { AGENT_PROFILE_NAME_PATTERN, type ChildStepPolicy, } from '../../domain/index.ts'; import { DEFAULT_CHILD_POLICY_ENVIRONMENT, isSafeStepCapabilityPath, isSafeStepResultPath, } from './child-policy-paths.ts'; import type { ChildPolicyEnvironment } from './child-policy-paths.ts'; import { parseChildPolicySections } from './child-policy-sections.ts'; const POLICY_DIGEST_PATTERN = /^[a-f0-9]{64}$/; const CAPABILITY_TOKEN_PATTERN = /^[a-f0-9]{64}$/; const POLICY_KEYS: ReadonlySet = new Set([ 'version', 'requestId', 'agent', 'workflowId', 'runId', 'stepId', 'stepTitle', 'cwd', 'policyDigest', 'capabilityPath', 'capabilityToken', 'resultPath', 'permissions', 'outcomes', 'pauseOutcomes', 'summaryMaxChars', 'maxToolCalls', 'handoffReserve', 'totalToolCalls', 'handoffOutcome', 'gateSubmitOutcome', 'workspace', ]); type RequiredStringField = | 'requestId' | 'agent' | 'workflowId' | 'runId' | 'stepId' | 'stepTitle' | 'cwd' | 'policyDigest' | 'capabilityPath' | 'capabilityToken' | 'resultPath'; const isRecord = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); const requiredString = ( value: Readonly>, field: RequiredStringField, ): string => { const candidate = value[field]; if (typeof candidate !== 'string' || !candidate) { throw new Error(`child policy ${field} must be a non-empty string`); } return candidate; }; /** * Returns whether a name follows the runtime naming contract used by * workflow agent profiles. */ export const isAgentProfileName = (name: string | undefined): name is string => Boolean(name && AGENT_PROFILE_NAME_PATTERN.test(name)); const rejectUnknownProperties = ( value: Readonly>, ): void => { const unknownKey = Object.keys(value).find((key) => !POLICY_KEYS.has(key)); if (unknownKey) { throw new Error(`child policy has unknown property "${unknownKey}"`); } }; type ToolBudget = Pick< ChildStepPolicy, 'maxToolCalls' | 'handoffReserve' | 'totalToolCalls' >; const parseToolBudget = ( value: Readonly>, ): ToolBudget => { const fields = [ value.maxToolCalls, value.handoffReserve, value.totalToolCalls, ]; if (fields.every((field) => field === undefined)) return {}; if (fields.some((field) => field === undefined)) { throw new Error( 'child policy tool budget fields must be provided together', ); } const [maxToolCalls, handoffReserve, totalToolCalls] = fields; if ( typeof maxToolCalls !== 'number' || !Number.isInteger(maxToolCalls) || maxToolCalls < 1 || maxToolCalls > 100_000 ) { throw new Error('child policy maxToolCalls is invalid'); } if (handoffReserve !== 2) { throw new Error('child policy handoffReserve is invalid'); } if ( typeof totalToolCalls !== 'number' || !Number.isInteger(totalToolCalls) || totalToolCalls !== maxToolCalls + handoffReserve ) { throw new Error('child policy totalToolCalls is invalid'); } return { maxToolCalls, handoffReserve, totalToolCalls }; }; type IdentityAndPaths = Pick< ChildStepPolicy, | 'version' | 'requestId' | 'agent' | 'workflowId' | 'runId' | 'stepId' | 'stepTitle' | 'cwd' | 'policyDigest' | 'capabilityPath' | 'capabilityToken' | 'resultPath' >; const parseIdentityAndPaths = ( value: Readonly>, environment: ChildPolicyEnvironment, ): IdentityAndPaths => { const requestId = requiredString(value, 'requestId'); const agent = requiredString(value, 'agent'); const workflowId = requiredString(value, 'workflowId'); const runId = requiredString(value, 'runId'); const stepId = requiredString(value, 'stepId'); const stepTitle = requiredString(value, 'stepTitle'); const cwd = requiredString(value, 'cwd'); const policyDigest = requiredString(value, 'policyDigest'); const capabilityPath = requiredString(value, 'capabilityPath'); const capabilityToken = requiredString(value, 'capabilityToken'); const resultPath = requiredString(value, 'resultPath'); if (value.version !== 1) throw new Error('unsupported child policy version'); if (!isAbsolute(cwd)) { throw new Error('child policy cwd must be an absolute path'); } if (!POLICY_DIGEST_PATTERN.test(policyDigest)) { throw new Error('child policy digest is invalid'); } if (!isAgentProfileName(agent)) { throw new Error('child policy agent is not a valid agent profile name'); } if (!CAPABILITY_TOKEN_PATTERN.test(capabilityToken)) { throw new Error('child policy capability token is invalid'); } if (!isSafeStepCapabilityPath(capabilityPath, environment)) { throw new Error( 'child policy capability path is outside its temporary directory', ); } if (!isSafeStepResultPath(resultPath, environment)) { throw new Error( 'child policy result path is outside its temporary directory', ); } if (dirname(resolve(capabilityPath)) !== dirname(resolve(resultPath))) { throw new Error('child policy files must share one temporary directory'); } return { version: 1, requestId, agent, workflowId, runId, stepId, stepTitle, cwd, policyDigest, capabilityPath, capabilityToken, resultPath, }; }; /** * Validates untrusted delegated policy data and returns its narrow domain type. * * @throws When the value does not satisfy the child policy contract. */ export const parseChildPolicy = ( value: unknown, environment: ChildPolicyEnvironment = DEFAULT_CHILD_POLICY_ENVIRONMENT, ): ChildStepPolicy => { if (!isRecord(value)) throw new Error('child policy must be an object'); rejectUnknownProperties(value); const sections = parseChildPolicySections(value); const toolBudget = parseToolBudget(value); const handoffOutcome = value.handoffOutcome; if (handoffOutcome !== undefined) { if (typeof handoffOutcome !== 'string' || handoffOutcome !== 'handoff') { throw new Error('child policy handoffOutcome is invalid'); } if (toolBudget.maxToolCalls === undefined) { throw new Error('child policy handoffOutcome requires a tool budget'); } if (!sections.outcomes.includes(handoffOutcome)) { throw new Error('child policy handoffOutcome must be an allowed outcome'); } } if (toolBudget.maxToolCalls !== undefined && handoffOutcome === undefined) { throw new Error('child policy tool budget requires handoffOutcome'); } return { ...parseIdentityAndPaths(value, environment), ...sections, ...toolBudget, ...(handoffOutcome ? { handoffOutcome } : {}), }; };