fix(onepassword): preserve pending tool authorization (#107275)

* fix(onepassword): preserve pending tool authorization

* refactor(onepassword): consume pending authorization in helper

* chore: defer release note generation
This commit is contained in:
Peter Steinberger
2026-07-14 01:12:04 -07:00
committed by GitHub
parent da9f974886
commit 3d15bf513e
3 changed files with 118 additions and 8 deletions
+74
View File
@@ -265,6 +265,80 @@ describe("OnePasswordBroker validation and policy", () => {
]);
});
it("authorizes when hook and execute contexts disagree on session fields", async () => {
const { broker, audit } = setup();
await broker.beforeToolCall(
{
toolName: "onepassword",
params: { action: "get", slug: "automatic", reason: "asymmetric contexts" },
toolCallId: "call_x|fc_y",
},
{
toolName: "onepassword",
toolCallId: "call_x|fc_y",
agentId: "main",
sessionKey: "agent:main:main",
sessionId: "hook-run-uuid",
},
);
await expect(
broker.get(
"call_x|fc_y",
{ action: "get", slug: "automatic", reason: "asymmetric contexts" },
{ agentId: "main", sessionKey: "agent:main:main" },
),
).resolves.toMatchObject({ slug: "automatic" });
expect((await audit.entries()).map((entry) => entry.value.outcome)).toEqual(["auto"]);
});
it("rejects an ambiguous fallback across sessions", async () => {
const { broker } = setup();
for (const sessionKey of ["session-a", "session-b"]) {
await broker.beforeToolCall(
{
toolName: "onepassword",
params: { action: "get", slug: "automatic", reason: "same request" },
toolCallId: "call-1",
},
{ toolName: "onepassword", toolCallId: "call-1", agentId: "agent-a", sessionKey },
);
}
await expect(
broker.get(
"call-1",
{ action: "get", slug: "automatic", reason: "same request" },
{ agentId: "agent-a", sessionKey: "session-c" },
),
).rejects.toMatchObject({ code: "POLICY_NOT_EVALUATED" });
});
it("rejects a unique fallback from another agent", async () => {
const { broker } = setup();
await broker.beforeToolCall(
{
toolName: "onepassword",
params: { action: "get", slug: "automatic", reason: "same request" },
toolCallId: "call-1",
},
{
toolName: "onepassword",
toolCallId: "call-1",
agentId: "agent-a",
sessionKey: "session-a",
},
);
await expect(
broker.get(
"call-1",
{ action: "get", slug: "automatic", reason: "same request" },
{ agentId: "agent-b", sessionKey: "session-b" },
),
).rejects.toMatchObject({ code: "POLICY_NOT_EVALUATED" });
});
it("persists allow-always grants and expires them", async () => {
const { broker, audit, grants, getItem, advance } = setup();
const first = await before(broker, "grant-1", {
+4 -8
View File
@@ -13,6 +13,7 @@ import {
} from "./config.js";
import { OnePasswordError, type OnePasswordErrorCode } from "./errors.js";
import type { OpClient, ResolvedSecret } from "./op-client.js";
import { takePendingAuthorization } from "./pending-authorization.js";
type AuditOutcome =
| "auto"
@@ -233,12 +234,8 @@ export class OnePasswordBroker {
private pendingKey(
context: Pick<AccessContext, "agentId" | "sessionKey" | "sessionId" | "toolCallId">,
): string {
return JSON.stringify([
context.agentId,
context.sessionKey,
context.sessionId,
context.toolCallId,
]);
const { agentId, sessionKey, sessionId, toolCallId } = context;
return JSON.stringify([agentId, sessionKey, sessionId, toolCallId]);
}
private async audit(
@@ -458,8 +455,7 @@ export class OnePasswordBroker {
reason: input.reason,
};
const key = this.pendingKey(fallbackContext);
const authorization = this.pending.get(key);
this.pending.delete(key);
const authorization = takePendingAuthorization(this.pending, key, fallbackContext);
if (
!authorization ||
authorization.slug !== input.slug ||
@@ -0,0 +1,40 @@
type PendingRequest = {
agentId: string;
toolCallId: string;
slug: string;
reason: string;
};
export function takePendingAuthorization<T extends PendingRequest>(
pending: Map<string, T>,
exactKey: string,
request: PendingRequest,
): T | undefined {
const exact = pending.get(exactKey);
if (exact) {
pending.delete(exactKey);
return exact;
}
let match: [string, T] | undefined;
for (const entry of pending) {
const candidate = entry[1];
if (
candidate.agentId !== request.agentId ||
candidate.toolCallId !== request.toolCallId ||
candidate.slug !== request.slug ||
candidate.reason !== request.reason
) {
continue;
}
if (match) {
return undefined;
}
match = entry;
}
if (!match) {
return undefined;
}
pending.delete(match[0]);
return match[1];
}