refactor(provider): flow branded ProviderID/ModelID through internal signatures (#17182)

This commit is contained in:
Kit Langton
2026-03-12 10:48:17 -04:00
committed by GitHub
parent a4f8d66a9b
commit 1cb7df7159
24 changed files with 227 additions and 205 deletions

View File

@@ -15,9 +15,13 @@ export namespace Permission {
return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern]
}
function covered(keys: string[], approved: Record<string, boolean>): boolean {
const pats = Object.keys(approved)
return keys.every((k) => pats.some((p) => Wildcard.match(k, p)))
function covered(keys: string[], approved: Map<string, boolean>): boolean {
return keys.every((k) => {
for (const p of approved.keys()) {
if (Wildcard.match(k, p)) return true
}
return false
})
}
export const Info = z
@@ -39,6 +43,12 @@ export namespace Permission {
})
export type Info = z.infer<typeof Info>
interface PendingEntry {
info: Info
resolve: () => void
reject: (e: any) => void
}
export const Event = {
Updated: BusEvent.define("permission.updated", Info),
Replied: BusEvent.define(
@@ -52,31 +62,13 @@ export namespace Permission {
}
const state = Instance.state(
() => {
const pending: {
[sessionID: string]: {
[permissionID: string]: {
info: Info
resolve: () => void
reject: (e: any) => void
}
}
} = {}
const approved: {
[sessionID: string]: {
[permissionID: string]: boolean
}
} = {}
return {
pending,
approved,
}
},
() => ({
pending: new Map<SessionID, Map<PermissionID, PendingEntry>>(),
approved: new Map<SessionID, Map<string, boolean>>(),
}),
async (state) => {
for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) {
for (const session of state.pending.values()) {
for (const item of session.values()) {
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
}
}
@@ -90,8 +82,8 @@ export namespace Permission {
export function list() {
const { pending } = state()
const result: Info[] = []
for (const items of Object.values(pending)) {
for (const item of Object.values(items)) {
for (const session of pending.values()) {
for (const item of session.values()) {
result.push(item.info)
}
}
@@ -114,9 +106,9 @@ export namespace Permission {
toolCallID: input.callID,
pattern: input.pattern,
})
const approvedForSession = approved[input.sessionID] || {}
const approvedForSession = approved.get(input.sessionID)
const keys = toKeys(input.pattern, input.type)
if (covered(keys, approvedForSession)) return
if (approvedForSession && covered(keys, approvedForSession)) return
const info: Info = {
id: PermissionID.ascending(),
type: input.type,
@@ -142,13 +134,13 @@ export namespace Permission {
return
}
pending[input.sessionID] = pending[input.sessionID] || {}
if (!pending.has(input.sessionID)) pending.set(input.sessionID, new Map())
return new Promise<void>((resolve, reject) => {
pending[input.sessionID][info.id] = {
pending.get(input.sessionID)!.set(info.id, {
info,
resolve,
reject,
}
})
Bus.publish(Event.Updated, info)
})
}
@@ -159,9 +151,11 @@ export namespace Permission {
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
log.info("response", input)
const { pending, approved } = state()
const match = pending[input.sessionID]?.[input.permissionID]
if (!match) return
delete pending[input.sessionID][input.permissionID]
const session = pending.get(input.sessionID)
const match = session?.get(input.permissionID)
if (!session || !match) return
session.delete(input.permissionID)
if (session.size === 0) pending.delete(input.sessionID)
Bus.publish(Event.Replied, {
sessionID: input.sessionID,
permissionID: input.permissionID,
@@ -173,30 +167,35 @@ export namespace Permission {
}
match.resolve()
if (input.response === "always") {
approved[input.sessionID] = approved[input.sessionID] || {}
if (!approved.has(input.sessionID)) approved.set(input.sessionID, new Map())
const approvedSession = approved.get(input.sessionID)!
const approveKeys = toKeys(match.info.pattern, match.info.type)
for (const k of approveKeys) {
approved[input.sessionID][k] = true
approvedSession.set(k, true)
}
const items = pending[input.sessionID]
const items = pending.get(input.sessionID)
if (!items) return
for (const item of Object.values(items)) {
const toRespond: Info[] = []
for (const item of items.values()) {
const itemKeys = toKeys(item.info.pattern, item.info.type)
if (covered(itemKeys, approved[input.sessionID])) {
respond({
sessionID: item.info.sessionID,
permissionID: item.info.id,
response: input.response,
})
if (covered(itemKeys, approvedSession)) {
toRespond.push(item.info)
}
}
for (const item of toRespond) {
respond({
sessionID: item.sessionID,
permissionID: item.id,
response: input.response,
})
}
}
}
export class RejectedError extends Error {
constructor(
public readonly sessionID: string,
public readonly permissionID: string,
public readonly sessionID: SessionID,
public readonly permissionID: PermissionID,
public readonly toolCallID?: string,
public readonly metadata?: Record<string, any>,
public readonly reason?: string,

View File

@@ -108,6 +108,12 @@ export namespace PermissionNext {
),
}
interface PendingEntry {
info: Request
resolve: () => void
reject: (e: any) => void
}
const state = Instance.state(() => {
const projectID = Instance.project.id
const row = Database.use((db) =>
@@ -115,17 +121,8 @@ export namespace PermissionNext {
)
const stored = row?.data ?? ([] as Ruleset)
const pending: Record<
string,
{
info: Request
resolve: () => void
reject: (e: any) => void
}
> = {}
return {
pending,
pending: new Map<PermissionID, PendingEntry>(),
approved: stored,
}
})
@@ -149,11 +146,11 @@ export namespace PermissionNext {
id,
...request,
}
s.pending[id] = {
s.pending.set(id, {
info,
resolve,
reject,
}
})
Bus.publish(Event.Asked, info)
})
}
@@ -170,9 +167,9 @@ export namespace PermissionNext {
}),
async (input) => {
const s = await state()
const existing = s.pending[input.requestID]
const existing = s.pending.get(input.requestID)
if (!existing) return
delete s.pending[input.requestID]
s.pending.delete(input.requestID)
Bus.publish(Event.Replied, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
@@ -182,9 +179,9 @@ export namespace PermissionNext {
existing.reject(input.message ? new CorrectedError(input.message) : new RejectedError())
// Reject all other pending permissions for this session
const sessionID = existing.info.sessionID
for (const [id, pending] of Object.entries(s.pending)) {
for (const [id, pending] of s.pending) {
if (pending.info.sessionID === sessionID) {
delete s.pending[id]
s.pending.delete(id)
Bus.publish(Event.Replied, {
sessionID: pending.info.sessionID,
requestID: pending.info.id,
@@ -211,13 +208,13 @@ export namespace PermissionNext {
existing.resolve()
const sessionID = existing.info.sessionID
for (const [id, pending] of Object.entries(s.pending)) {
for (const [id, pending] of s.pending) {
if (pending.info.sessionID !== sessionID) continue
const ok = pending.info.patterns.every(
(pattern) => evaluate(pending.info.permission, pattern, s.approved).action === "allow",
)
if (!ok) continue
delete s.pending[id]
s.pending.delete(id)
Bus.publish(Event.Replied, {
sessionID: pending.info.sessionID,
requestID: pending.info.id,
@@ -283,6 +280,6 @@ export namespace PermissionNext {
export async function list() {
const s = await state()
return Object.values(s.pending).map((x) => x.info)
return Array.from(s.pending.values(), (x) => x.info)
}
}