55 approvalCoversSubject ,
66 evaluateApprovals ,
77 grantScopeMatches ,
8+ normalizeSeededApprovals ,
89 cwdMatchesGrant ,
910 type GrantWorkspace ,
1011} from "./authz-grants.js" ;
@@ -27,6 +28,8 @@ describe("grant tool/providerModel/cwd scoping agrees across call sites", () =>
2728 { tool : "run_shell" , pattern : "npm test" , providerModel : "openai:gpt-5" } ,
2829 { tool : "run_shell" , pattern : "npm test" , cwd : "/proj" } ,
2930 { tool : "write_file" , pattern : "npm test" } ,
31+ { tool : "update_plan" , pattern : "npm test" } ,
32+ { tool : "manage_tasks" , pattern : "npm test" } ,
3033 ] ;
3134
3235 const requests : {
@@ -39,6 +42,8 @@ describe("grant tool/providerModel/cwd scoping agrees across call sites", () =>
3942 { tool : "run_shell" , cwd : "/other" , activeProviderModel : "openai:gpt-5" } ,
4043 { tool : "run_shell" , cwd : undefined , activeProviderModel : undefined } ,
4144 { tool : "write_file" , cwd : "/proj" , activeProviderModel : undefined } ,
45+ { tool : "manage_tasks" , cwd : "/proj" , activeProviderModel : undefined } ,
46+ { tool : "update_plan" , cwd : "/proj" , activeProviderModel : undefined } ,
4247 ] ;
4348
4449 for ( const grant of grants ) {
@@ -85,6 +90,86 @@ describe("grant tool/providerModel/cwd scoping agrees across call sites", () =>
8590 } ) ;
8691 }
8792 }
93+
94+ test ( "stored update_plan grant never covers a manage_tasks request" , async ( ) => {
95+ const grant : Approval = { tool : "update_plan" , pattern : "*" } ;
96+ expect (
97+ grantScopeMatches ( grant , "manage_tasks" , undefined , "/proj" , workspace ) ,
98+ ) . toBe ( false ) ;
99+ expect (
100+ await approvalCoversSubject ( {
101+ tool : "manage_tasks" ,
102+ subject : "manage_tasks" ,
103+ approvals : [ grant ] ,
104+ activeProviderModel : undefined ,
105+ requestCwd : "/proj" ,
106+ workspace,
107+ } ) ,
108+ ) . toBe ( false ) ;
109+ } ) ;
110+
111+ test ( "stored manage_tasks grant covers an update_plan-presenting request" , async ( ) => {
112+ const grant : Approval = { tool : "manage_tasks" , pattern : "*" } ;
113+ expect (
114+ grantScopeMatches ( grant , "update_plan" , undefined , "/proj" , workspace ) ,
115+ ) . toBe ( true ) ;
116+ expect (
117+ await approvalCoversSubject ( {
118+ tool : "update_plan" ,
119+ subject : "update_plan" ,
120+ approvals : [ grant ] ,
121+ activeProviderModel : undefined ,
122+ requestCwd : "/proj" ,
123+ workspace,
124+ } ) ,
125+ ) . toBe ( true ) ;
126+ } ) ;
127+ } ) ;
128+
129+ // Seeded grants enter the gate in native key space. Pure renames collapse onto
130+ // the engine id (capability-identical, behavior-preserving); a stored
131+ // update_plan key has no native representation that preserves its narrow
132+ // create-only capability, so it is dropped fail-closed instead of widening
133+ // onto manage_tasks.
134+ describe ( "normalizeSeededApprovals" , ( ) => {
135+ test ( "collapses pure renames onto the engine id, preserving other fields" , ( ) => {
136+ expect (
137+ normalizeSeededApprovals ( [
138+ {
139+ tool : "bash" ,
140+ pattern : "npm test" ,
141+ providerModel : "openai:gpt-5" ,
142+ cwd : "/proj" ,
143+ } ,
144+ { tool : "read" , pattern : "README.md" } ,
145+ ] ) ,
146+ ) . toEqual ( [
147+ {
148+ tool : "run_shell" ,
149+ pattern : "npm test" ,
150+ providerModel : "openai:gpt-5" ,
151+ cwd : "/proj" ,
152+ } ,
153+ { tool : "read_file" , pattern : "README.md" } ,
154+ ] ) ;
155+ } ) ;
156+
157+ test ( "keeps native keys as-is" , ( ) => {
158+ const seeded : Approval [ ] = [
159+ { tool : "run_shell" , pattern : "*" } ,
160+ { tool : "manage_tasks" , pattern : "*" } ,
161+ ] ;
162+ expect ( normalizeSeededApprovals ( seeded ) ) . toEqual ( seeded ) ;
163+ } ) ;
164+
165+ test ( "drops update_plan keys fail-closed" , ( ) => {
166+ expect (
167+ normalizeSeededApprovals ( [
168+ { tool : "update_plan" , pattern : "*" } ,
169+ { tool : "manage_tasks" , pattern : "*" } ,
170+ ] ) ,
171+ ) . toEqual ( [ { tool : "manage_tasks" , pattern : "*" } ] ) ;
172+ } ) ;
88173} ) ;
89174
90175// Grant minting decomposes a multi-segment chain scope into one grant per
0 commit comments