mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
da9f974886
commit
3d15bf513e
@@ -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", {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user