Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/framework/core/tools/amadeus-bolt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,9 @@ function handleSetAutonomy(args: string[], explicitProjectDir?: string): void {
if (!["none", "semi", "full"].includes(flags.mode)) {
error(`Invalid --mode: ${flags.mode}. Must be 'none', 'semi', or 'full'.`);
}
// Reported before the file is read: a mode with no carrier for policies is a
// mismatch worth naming ahead of anything wrong inside the file itself.
if (flags.mode === "none" && flags["policies-file"] !== undefined) error("--policies-file is not accepted with --mode none (policies have no carrier in mode none).");

const pd = resolveBoltProjectDir(explicitProjectDir);

Expand Down
29 changes: 19 additions & 10 deletions packages/framework/core/tools/amadeus-intent-autonomy-production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { basename } from "node:path";

import {
autonomyDigest,
autonomyScopeFingerprint,
authorizeInteraction,
createAutonomyProjection,
createDecisionOptionEffectRegistry,
createInteractionOccurrence,
grantIssuanceDisplayDigest,
nonFullCommandDisplayDigest,
normalizeDecisionPolicies,
SEMI_ROUTINE_INTERACTIONS,
type AutonomyMode,
Expand Down Expand Up @@ -286,7 +288,7 @@ export function fallbackFingerprints(
scopeId: string,
): { readonly scopeFingerprint: string; readonly normFingerprint: string } {
return {
scopeFingerprint: autonomyDigest({ intentUuid, scopeId }),
scopeFingerprint: autonomyScopeFingerprint(intentUuid, scopeId),
normFingerprint: autonomyDigest({ scopeId, rules: "resolved-rules-in-context-v1" }),
};
}
Expand Down Expand Up @@ -393,19 +395,26 @@ function prepareFullGrantCommand(input: PrepareFullGrantCommandInput): { readonl
};
}

// The policies stay raw here: planHumanAutonomyCommand owns the one
// normalization call, and the digest is computed over the same raw set on both
// sides so the confirmation compares like with like.
function prepareNonFullCommand(
before: AutonomyProjection,
mode: Exclude<AutonomyMode, "full">,
policies: readonly DecisionPolicyInput[],
): { readonly command: HumanAutonomyCommand; readonly displayDigest: string } {
if (before.currentGrant !== null) {
return {
command: { kind: "revoke-full", targetMode: mode },
displayDigest: autonomyDigest({ intentUuid: before.intentUuid, mode, revoke: before.currentGrant.grantId }),
};
}
const revokedGrantId = before.currentGrant?.grantId ?? null;
const displayDigest = nonFullCommandDisplayDigest({
intentUuid: before.intentUuid,
mode,
revokedGrantId,
policies,
});
return {
command: { kind: "set-mode", mode },
displayDigest: autonomyDigest({ intentUuid: before.intentUuid, mode }),
command: revokedGrantId === null
? { kind: "set-mode", mode, policies }
: { kind: "revoke-full", targetMode: mode, policies },
displayDigest,
};
}

Expand Down Expand Up @@ -441,7 +450,7 @@ export function applyProductionAutonomyMode(input: ApplyProductionAutonomyModeIn
command = prepared.command;
confirmedDisplayDigest = prepared.issuanceDigest;
} else {
const prepared = prepareNonFullCommand(before, input.mode);
const prepared = prepareNonFullCommand(before, input.mode, input.policies ?? []);
command = prepared.command;
confirmedDisplayDigest = prepared.displayDigest;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
resolveAutoDecision,
revalidateGrantExerciseReservation,
SemiAuthority,
semiPoliciesOf,
validateResumeCondition,
type AutoDecisionRecord,
type AutonomyProjection,
Expand Down Expand Up @@ -791,6 +792,9 @@ export interface IntentAutonomyStatusEnvelope {
readonly resumeCondition: ResumeCondition | null;
readonly legacyStandingGrantCount: number;
readonly unreviewedAutoDecisionCount: number;
// Grant-independent: semi carries its own policies, so the count cannot be
// read off the grant alone.
readonly policyCount: number;
readonly terminalLiveCompletionCapable: true;
}

Expand All @@ -810,6 +814,7 @@ export function projectIntentAutonomyStatus(projection: AutonomyProjection): Int
resumeCondition: projection.parkEnvelope?.resumeCondition ?? null,
legacyStandingGrantCount: projection.legacyStandingGrantIds.length,
unreviewedAutoDecisionCount: projection.autoDecisions.filter((decision) => decision.reviewState === "unreviewed").length,
policyCount: grant?.policies.length ?? semiPoliciesOf(projection).length,
terminalLiveCompletionCapable: true,
};
}
107 changes: 99 additions & 8 deletions packages/framework/core/tools/amadeus-intent-autonomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,25 @@ export function createAutonomyProjection(input: CreateAutonomyProjectionInput):
return projection;
}

// The non-full arms carry raw policy inputs: their normalization seed is the
// command occurrence, which only exists inside planHumanAutonomyCommand. The
// full arms are normalized earlier because grant issuance seeds off the grant.
export type HumanAutonomyCommand =
| { readonly kind: "set-mode"; readonly mode: "none" | "semi" }
| {
readonly kind: "set-mode";
readonly mode: "none" | "semi";
readonly policies: readonly DecisionPolicyInput[];
}
| {
readonly kind: "issue-full" | "replace-full";
readonly scope: GrantScopeDescriptor;
readonly policies: readonly DecisionPolicy[];
}
| { readonly kind: "revoke-full"; readonly targetMode: "none" | "semi" };
| {
readonly kind: "revoke-full";
readonly targetMode: "none" | "semi";
readonly policies: readonly DecisionPolicyInput[];
};

export interface HumanCommandContext {
readonly targetIntentUuid: string;
Expand Down Expand Up @@ -349,6 +360,87 @@ export function grantIssuanceDisplayDigest(input: GrantIssuanceDisplayDigestInpu
return autonomyDigest({ ...input, policySetDigest: autonomyDigest(input.policies) });
}

// The scope id semi policies are normalized against. semi holds no grant scope,
// so the carrier borrows the intent-wide fingerprint that SemiAuthorityScope
// also carries at decision time; were the two to diverge, the confirmed-policy
// rung would filter every policy out without saying so.
export const SEMI_POLICY_SCOPE_ID = "intent";

export function autonomyScopeFingerprint(intentUuid: string, scopeId: string): string {
return autonomyDigest({ intentUuid, scopeId });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

interface NonFullCommandDisplayDigestInput {
readonly intentUuid: string;
readonly mode: Exclude<AutonomyMode, "full">;
readonly revokedGrantId: string | null;
readonly policies: readonly DecisionPolicyInput[];
}

// One definition for both non-full previews (plain mode set, and grant revoke).
// Same shape as grantIssuanceDisplayDigest, minus principalId and scope: semi
// has no grant scope, and folding the policy set in is what makes a swapped
// policy set visible to the confirmation check below.
export function nonFullCommandDisplayDigest(input: NonFullCommandDisplayDigestInput): string {
return autonomyDigest({ ...input, policySetDigest: autonomyDigest(input.policies) });
}

type NonFullAutonomyCommand = Extract<HumanAutonomyCommand, { readonly kind: "set-mode" | "revoke-full" }>;

function isNonFullCommand(command: HumanAutonomyCommand): command is NonFullAutonomyCommand {
return command.kind === "set-mode" || command.kind === "revoke-full";
}

function nonFullTargetMode(command: NonFullAutonomyCommand): Exclude<AutonomyMode, "full"> {
return command.kind === "set-mode" ? command.mode : command.targetMode;
}

// Zero policies keeps the historic single-step confirmation shape; a carried
// policy set has to be the very set the human was shown.
function nonFullConfirmationHolds(
projection: AutonomyProjection,
command: NonFullAutonomyCommand,
context: HumanCommandContext,
): boolean {
if (command.policies.length === 0) return true;
return context.confirmedDisplayDigest === nonFullCommandDisplayDigest({
intentUuid: projection.intentUuid,
mode: nonFullTargetMode(command),
revokedGrantId: projection.currentGrant?.grantId ?? null,
policies: command.policies,
});
}

// The single normalization call site for semi policies. Normalizing anywhere
// else would let the digest the human confirmed and the set that is stored
// drift apart.
function semiPoliciesAfter(
projection: AutonomyProjection,
command: HumanAutonomyCommand,
context: HumanCommandContext,
): readonly DecisionPolicy[] | undefined {
if (!isNonFullCommand(command)) return undefined;
if (nonFullTargetMode(command) !== "semi" || command.policies.length === 0) return undefined;
return normalizeDecisionPolicies({
grantIdentitySeed: context.commandOccurrenceId,
scopeFingerprint: autonomyScopeFingerprint(projection.intentUuid, SEMI_POLICY_SCOPE_ID),
humanTurnId: context.humanTurn.turnId,
policies: command.policies,
});
}

// Absent, never present-and-undefined: an undefined-valued key changes the
// canonical digest but is dropped by JSON round trips, which would break the
// audit replay digest check.
function withSemiPolicies(
projection: AutonomyProjection,
policies: readonly DecisionPolicy[] | undefined,
): AutonomyProjection {
const rest: { semiPolicies?: readonly DecisionPolicy[] } & AutonomyProjection = { ...projection };
delete rest.semiPolicies;
return policies === undefined ? rest : { ...rest, semiPolicies: policies };
}

export function planHumanAutonomyCommand(
projection: AutonomyProjection,
command: HumanAutonomyCommand,
Expand All @@ -362,12 +454,11 @@ export function planHumanAutonomyCommand(
if ((command.kind === "replace-full" || command.kind === "revoke-full") && current === null) {
return { ok: false, code: "INVALID_COMMAND" };
}
if (isNonFullCommand(command) && !nonFullConfirmationHolds(projection, command, context)) {
return { ok: false, code: "INVALID_COMMAND" };
}
try {
const afterMode: AutonomyMode = command.kind === "set-mode"
? command.mode
: command.kind === "revoke-full"
? command.targetMode
: "full";
const afterMode: AutonomyMode = isNonFullCommand(command) ? nonFullTargetMode(command) : "full";
const provenance: ModeProvenance = {
kind: "human-command",
principalId: context.principalId,
Expand All @@ -383,7 +474,7 @@ export function planHumanAutonomyCommand(
? issueGrant(projection, command, context)
: null;
const after: AutonomyProjection = {
...projection,
...withSemiPolicies(projection, semiPoliciesAfter(projection, command, context)),
mode: afterMode,
modeProvenance: provenance,
currentGrant: issuedGrant,
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/core/tools/amadeus-utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ function renderAutonomyStatus(
`Grant: ${autonomy.grant === null ? "none" : `${autonomy.grant.id} (${autonomy.grant.state})`}`,
`Grant Scope: ${autonomy.grant?.scopeFingerprint ?? "none"}`,
`Workflow State: ${autonomy.workflowExecutionState ?? "completed"}`,
`Policies: ${autonomy.grant?.policyCount ?? 0}`,
`Policies: ${autonomy.policyCount}`,
`Unreviewed: ${autonomy.unreviewedAutoDecisionCount}`,
`Stop Reason: ${autonomy.suspendedReason ?? "none"}`,
`Resume: ${autonomy.resumeCondition === null ? "none" : JSON.stringify(autonomy.resumeCondition)}`,
Expand Down
2 changes: 1 addition & 1 deletion tests/.coverage-patch-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -5717,7 +5717,7 @@
"file": "packages/framework/core/tools/amadeus-utility.ts",
"selector": {
"function": "renderAutonomyStatus",
"fingerprint": "sha256:92a73d7aee92b2287fca5cfded8db53471325ce43e37ba7473fe46c23e071a8e",
"fingerprint": "sha256:afaafc3d3e26d2217d1cb82b26f13e1fb9adebf205ba10f421dda7017c3b94f8",
"anchorLines": 13,
"targetLines": "1-13"
},
Expand Down
4 changes: 4 additions & 0 deletions tests/.coverage-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -5811,6 +5811,10 @@
"file": "tests/integration/t435-intent-autonomy-production.integration.test.ts",
"mechanism": "cli"
},
{
"file": "tests/integration/t455-semi-policy-cli.integration.test.ts",
"mechanism": "cli"
},
{
"file": "tests/unit/t33.test.ts",
"mechanism": "cli"
Expand Down
1 change: 1 addition & 0 deletions tests/integration/t-coverage-mechanism-ratchet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ describe("repository-wide mechanism honesty ratchets", () => {
"integration/t429-legacy-goal-migration.integration.test.ts",
"integration/t433-autonomy-review-observability.test.ts",
"integration/t45.test.ts",
"integration/t455-semi-policy-cli.integration.test.ts",
"integration/t49.test.ts",
"integration/t51.test.ts",
"integration/t66.test.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe("Intent autonomy durable coordinator", () => {
const initial = createAutonomyProjection({ intentUuid: INTENT });
const repository = createMemoryIntentAutonomyRepository();
const coordinator = createIntentAutonomyCoordinator({ initialProjection: initial, repository });
const result = coordinator.applyHumanCommand({ kind: "set-mode", mode: "semi" }, {
const result = coordinator.applyHumanCommand({ kind: "set-mode", mode: "semi", policies: [] }, {
targetIntentUuid: INTENT,
principalId: "principal-1",
humanTurn: { verified: true, eventType: "HUMAN_TURN", actor: "human", turnId: "human-turn-1" },
Expand Down Expand Up @@ -312,7 +312,7 @@ describe("Intent autonomy durable coordinator", () => {
initialProjection: initial,
repository: createMemoryIntentAutonomyRepository(),
});
const semiCommand = semiCoordinator.applyHumanCommand({ kind: "set-mode", mode: "semi" }, {
const semiCommand = semiCoordinator.applyHumanCommand({ kind: "set-mode", mode: "semi", policies: [] }, {
targetIntentUuid: INTENT,
principalId: "principal-1",
humanTurn: { verified: true, eventType: "HUMAN_TURN", actor: "human", turnId: "human-turn-1" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function semiRuntime(): {
const initial = createAutonomyProjection({ intentUuid: INTENT });
const repository = createMemoryIntentAutonomyRepository();
const coordinator = createIntentAutonomyCoordinator({ initialProjection: initial, repository });
const applied = coordinator.applyHumanCommand({ kind: "set-mode", mode: "semi" }, {
const applied = coordinator.applyHumanCommand({ kind: "set-mode", mode: "semi", policies: [] }, {
targetIntentUuid: INTENT,
principalId: "principal-1",
humanTurn: { verified: true, eventType: "HUMAN_TURN", actor: "human", turnId: "human-turn-1" },
Expand Down
Loading
Loading