Skip to content

Commit 2394c45

Browse files
authored
Merge branch 'main' into feat/workflow-enter-origin
2 parents e2a3309 + f35061e commit 2394c45

6 files changed

Lines changed: 345 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pythoughts/pythinker-code": patch
3+
---
4+
5+
Model permission deny rules now also apply to subagent model overrides coming from agent profiles and from resume or retry, not only to models named in tool arguments; a denied override falls back to the parent agent's model.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pythoughts/pythinker-code": patch
3+
---
4+
5+
Subagent lifecycle events now carry the workflow name on start, completion and failure, and suspension events carry both the workflow run id and name, so clients can correlate every event without caching the spawn event.

packages/agent-core/src/agent/permission/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Agent } from '..';
22
import type { PrepareToolExecutionResult } from '../../loop';
33
import { createHookIfMatcher } from '../../session/hooks';
4+
import { matchesGlobRuleSubjects, modelRuleSubject } from '../../tools/support/rule-match';
5+
import { matchPermissionRule } from './matches-rule';
46
import { createPermissionDecisionPolicies } from './policies';
57
import type {
68
ApprovalResponse,
@@ -57,6 +59,40 @@ export class PermissionManager {
5759
};
5860
}
5961

62+
/**
63+
* Whether a deny rule forbids running a subagent on `modelAlias`, asked
64+
* outside the tool-approval path.
65+
*
66+
* Deny rules with an argument pattern fire at approval only when their
67+
* subject appears in the tool arguments. A model resolved after approval —
68+
* a subagent profile's override, or a resume/retry that re-resolves it —
69+
* never comes back through approval, so the spawn path re-checks it here
70+
* against the same rules.
71+
*
72+
* Only rules whose argument pattern targets the `model:` namespace are
73+
* consulted. Approval evaluates every rule against the call's full subject
74+
* set (profile name, plan digest, model); this check sees only the model, so
75+
* a rule keyed on another subject — `Agent(!reviewer)`, a workflow plan
76+
* digest — must not be re-interpreted here: its negation would match any
77+
* model-only subject list and strip an override approval already allowed.
78+
*/
79+
deniesModelOverride(toolName: string, modelAlias: string): boolean {
80+
const subjects = modelRuleSubject(modelAlias);
81+
if (subjects.length === 0) return false;
82+
return this.effectiveRules.some(
83+
(rule) =>
84+
rule.decision === 'deny' &&
85+
matchPermissionRule({
86+
rule,
87+
toolName,
88+
execution: {
89+
matchesRule: (ruleArgs) =>
90+
targetsModelSubject(ruleArgs) && matchesGlobRuleSubjects(ruleArgs, subjects),
91+
},
92+
})?.hasRuleArgs === true,
93+
);
94+
}
95+
6096
setMode(mode: PermissionMode): void {
6197
this.agent.records.logRecord({
6298
type: 'permission.set_mode',
@@ -368,3 +404,9 @@ export class PermissionManager {
368404
return prefix;
369405
}
370406
}
407+
408+
/** Whether a rule argument pattern (optionally negated) targets the `model:` subject namespace. */
409+
function targetsModelSubject(ruleArgs: string): boolean {
410+
const positive = ruleArgs.startsWith('!') ? ruleArgs.slice(1) : ruleArgs;
411+
return positive.startsWith('model:');
412+
}

packages/agent-core/src/session/subagent-host.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ export class SessionSubagentHost {
361361
// All subagent lifecycle events carry the launching tool call id so
362362
// consumers can correlate them to a workflow and drop stale events.
363363
parentToolCallId: event.task.parentToolCallId,
364+
workflowRunId: event.task.workflowRunId,
365+
workflowName: event.task.workflowName,
364366
reason: event.reason,
365367
});
366368
}
@@ -437,16 +439,28 @@ export class SessionSubagentHost {
437439
* SingleModelProvider) falls back to the parent's model instead of failing at
438440
* generate time. fastMode stays a straight inherit: it is a preference the
439441
* provider layer already drops when the active model cannot serve it.
442+
*
443+
* A `model:` deny rule is re-checked here as well — approval only sees a
444+
* model that was in the tool arguments, so a profile-sourced override (or a
445+
* resume/retry re-resolution) would otherwise ride past `Agent(model:x)` /
446+
* `DynamicWorkflow(model:x)`. Every override lands in this method, making it
447+
* the one containment point; a denied override falls back to the parent's
448+
* model rather than failing the spawn.
440449
*/
441450
private childModelConfig(
442451
parent: Agent,
443452
child: Agent,
444453
profile: ResolvedAgentProfile | undefined,
445-
options: Pick<RunSubagentOptions, 'modelAlias' | 'thinkingLevel'>,
454+
options: Pick<RunSubagentOptions, 'modelAlias' | 'thinkingLevel' | 'workflowRunId'>,
446455
): { modelAlias: string | undefined; thinkingLevel: string | undefined; fastMode: boolean } {
447456
const requested = options.modelAlias ?? profile?.model;
448457
const modelAlias =
449-
requested !== undefined && child.config.canResolveModel(requested)
458+
requested !== undefined &&
459+
child.config.canResolveModel(requested) &&
460+
!parent.permission.deniesModelOverride(
461+
options.workflowRunId === undefined ? 'Agent' : 'DynamicWorkflow',
462+
requested,
463+
)
450464
? requested
451465
: parent.config.modelAlias;
452466
return {
@@ -562,6 +576,7 @@ export class SessionSubagentHost {
562576
subagentId: childId,
563577
parentToolCallId: options.parentToolCallId,
564578
workflowRunId: options.workflowRunId,
579+
workflowName: options.workflowName,
565580
resultSummary: result,
566581
usage,
567582
contextTokens: child.context.tokenCount,
@@ -712,6 +727,7 @@ export class SessionSubagentHost {
712727
subagentId: childId,
713728
parentToolCallId: options.parentToolCallId,
714729
workflowRunId: options.workflowRunId,
730+
workflowName: options.workflowName,
715731
});
716732
}
717733

@@ -727,6 +743,7 @@ export class SessionSubagentHost {
727743
subagentId: childId,
728744
parentToolCallId: options.parentToolCallId,
729745
workflowRunId: options.workflowRunId,
746+
workflowName: options.workflowName,
730747
error: error instanceof Error ? error.message : String(error),
731748
});
732749
}

0 commit comments

Comments
 (0)