/** * Commitment Approval Guard — code-level enforcement of the orchestrator's * "launching a team needs explicit owner approval" boundary. * * WHY THIS EXISTS (2026-06-02 autonomy incident, recurrence of 2026-05-30): * The orchestrator prompt already forbids launching a team without an explicit * affirmative owner directive, and forbids fabricating approval. The orc * violated both anyway — it wrote `owner Steve 拍板「启动 Phase 1」` into a * delegate WorkItem and cold-started a dormant team, with NO such approving * message anywhere in the owner's chat history. Prompt rules have been re-tried * across incidents and keep failing: a code gate is the only structural * enforcement. * * THE KEY INSIGHT: the orc can fabricate *text* (a WorkItem title, a Slack * message), but it cannot fabricate the *owner's chat history*. So we gate the * commitment chokepoint — cold-starting a DORMANT team, which is exactly the * prompt's "launching a team / spinning up agents for a new project" class — * on a genuine recent owner (`user`-authored) affirmative message. Continuation * (a team that already has active members) and crash recovery (rehydrate) are * NOT gated. * * This module is intentionally PURE (no I/O) so the decision logic is fully * unit-testable; the caller supplies the team and the owner's recent messages. * * @module services/orchestrator/commitment-approval-guard */ /** * How far back to look for an owner approval, in ms. Generous (6h) on purpose: * a legitimate launch usually follows the owner's "go" within minutes-to-hours, * and a wide window minimizes false-blocks of legitimate re-wakes while still * blocking the incident class (a launch with NO owner approval anywhere). */ export declare const COMMITMENT_APPROVAL_LOOKBACK_MS: number; /** A minimal view of a team member — only the fields the guard needs. */ export interface GuardTeamMember { agentStatus: string; /** Why the member last went inactive (see TeamMember.dropoutReason) */ dropoutReason?: string; /** Last change to the member record (ISO) */ updatedAt?: string; } /** A minimal view of a team — only the fields the guard needs. */ export interface GuardTeam { members: GuardTeamMember[]; } /** Result of an approval evaluation. */ export interface ApprovalDecision { /** Whether the launch may proceed. */ allowed: boolean; /** Human-readable reason (always set when `allowed` is false). */ reason?: string; /** The owner message text that satisfied the gate, when allowed via approval. */ evidence?: string; } /** * Returns true if the team is dormant — no member is in an active-like status. * Starting a member of a dormant team is a COLD LAUNCH (commitment); starting a * member of a team that already has live members is continuation. * * @param team - The team whose members to inspect. * @returns True when no member is active/started/starting/activating. */ export declare function isDormantTeam(team: GuardTeam): boolean; /** * Returns true if `content` contains a clear owner-affirmative approval token. * * @param content - A chat message's text. * @returns True when the text carries an unambiguous launch approval. */ export declare function containsApprovalToken(content: string): boolean; /** * Evaluates whether a cold team launch is permitted. * * The launch is BLOCKED only when BOTH: * - the team is dormant (this start is a cold launch / "launching a team"), AND * - none of the supplied recent owner messages carries an approval token. * * If the team is already active (continuation) the launch is always allowed. * If a genuine owner approval is present, the launch is allowed and the * matching message is returned as `evidence`. * * @param args.team - The team being launched. * @param args.recentOwnerMessages - Recent OWNER-authored message texts * (already time-windowed and filtered to `sender_type='user'` by the caller). * @returns The approval decision. */ export declare function evaluateColdLaunch(args: { team: GuardTeam; recentOwnerMessages: string[]; }): ApprovalDecision; /** A minimal view of a WorkItem — only the fields the exemption needs. */ export interface GuardWorkItem { /** WorkItem type, e.g. 'cron_run' for scheduled runs. */ type?: string; /** Trigger id, set when an event/time trigger created/woke the WorkItem. */ triggerId?: string; /** Extensible metadata; cron runs carry `source:'cron'` + `cronTaskId`. */ metadata?: Record | null; } /** A WorkItem's self-declared scheduled origin, pending registry verification. */ export interface ScheduleClaim { /** Which scheduler the WorkItem claims to originate from. */ kind: 'cron' | 'trigger'; /** The id to verify against the real scheduler registry (unfakeable check). */ refId: string; } /** * Extracts a WorkItem's claimed scheduled origin, if any. * * Returns the backing cron-task / trigger id that the CALLER must then verify * against the real scheduler registry. Returns null when the WorkItem makes no * verifiable scheduled-origin claim (e.g. a `cron_run` with no `cronTaskId`, or * an ordinary delegate WorkItem) — such items get no exemption. * * @param wi - The WorkItem that triggered the wake. * @returns The schedule claim to verify, or null if none. */ export declare function extractScheduleClaim(wi: GuardWorkItem): ScheduleClaim | null; /** * Decides whether a wake is pre-authorized by a real owner-configured schedule. * * The decision is the whole exemption logic, made unit-testable by injecting the * registry lookups: a claim is honored ONLY when its id resolves in the real * scheduler, so a forged `source:'cron'` claim with a non-existent id is * rejected and falls through to the commitment gate. * * @param wi - The WorkItem that triggered the wake (may be null). * @param registry - Lookups against the REAL scheduler registry. * @param registry.cronTaskExists - Resolves true iff the cron task id is registered. * @param registry.triggerExists - True iff the trigger id is registered. * @returns True when the wake should bypass the commitment gate. */ export declare function isPreAuthorizedSchedule(wi: GuardWorkItem | null | undefined, registry: { cronTaskExists: (id: string) => Promise; triggerExists: (id: string) => boolean; }): Promise; /** The WorkItem a wake is for, as the parked-resume check sees it. */ export interface GuardWakeWorkItem { status: string; target?: string | null; /** Who created it (`orchestrator`, `team_lead`, `agent`, `system`) */ owner?: string; } /** * Whether starting this member RESUMES a team the harness parked, rather * than cold-launching it (CREW-303 / CREW-304). * * The harness stops idle members on its own (idle stop, freeing a slot under * the running-agent cap, a Crewly update). When it stops the last one, the * team looks dormant, and the reconciler's own wake for that team's queued * work was refused as a cold launch needing the owner's approval. On * 2026-10-06 the CE team sat like that for 4h35m with verify work queued, * and only restarted because the owner happened to say "启动" about a * different team. * * Resuming is allowed only when ALL hold: * - the wake names a WorkItem that is queued or blocked FOR THIS MEMBER; * - that WorkItem was not created by the orchestrator (the 2026-06-02 * incident class — the orchestrator launching a team on its own — stays * gated); * - a member of the team was parked by the harness (one of `parkReasons`) * within `windowMs`: the team was running recently and nobody stopped it * on purpose (an owner stop is `manual` and does not count). * * @param args.team - The team (members with status, dropout reason, update time) * @param args.sessionName - Session of the member being started * @param args.workItem - The WorkItem the wake is for, or null * @param args.now - Clock (epoch ms) * @param args.parkReasons - Dropout reasons that mean the harness parked a member * @param args.windowMs - How long after a park the team still counts as resuming * @returns True when the start is a resume and needs no fresh owner approval */ export declare function isHarnessParkedResume(args: { team: GuardTeam; sessionName: string | undefined; workItem: GuardWakeWorkItem | null | undefined; now: number; parkReasons: readonly string[]; windowMs: number; }): boolean; //# sourceMappingURL=commitment-approval-guard.d.ts.map