Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84353d3343 |
+14
-1
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
classifyRun,
|
||||
decideToolCall,
|
||||
resolveSenderId,
|
||||
runKey,
|
||||
} from "./policy.js";
|
||||
|
||||
@@ -34,6 +35,14 @@ export default {
|
||||
const config = event.context?.pluginConfig ?? api.pluginConfig ?? {};
|
||||
const trust = classifyRun(ctx, config);
|
||||
const key = runKey(event, ctx);
|
||||
const existing = key ? activeRuns.get(key) : undefined;
|
||||
if (existing && existing.senderId !== trust.senderId) {
|
||||
return {
|
||||
outcome: "block",
|
||||
reason: "run_scope_sender_conflict",
|
||||
message: "This run scope is already bound to a different authenticated sender.",
|
||||
};
|
||||
}
|
||||
if (key) {
|
||||
activeRuns.set(key, { ...trust, recordedAt: Date.now() });
|
||||
}
|
||||
@@ -60,7 +69,11 @@ export default {
|
||||
prune(activeRuns);
|
||||
const config = event.context?.pluginConfig ?? api.pluginConfig ?? {};
|
||||
const key = runKey(event, ctx);
|
||||
const trust = key ? activeRuns.get(key) : undefined;
|
||||
let trust = key ? activeRuns.get(key) : undefined;
|
||||
const senderId = resolveSenderId(ctx);
|
||||
if (trust && senderId && trust.senderId !== senderId) {
|
||||
trust = { trusted: false, reason: "run_scope_sender_conflict" };
|
||||
}
|
||||
const decision = decideToolCall(event.toolName, trust, config);
|
||||
|
||||
if (decision.action === "block") {
|
||||
|
||||
+20
-5
@@ -66,9 +66,24 @@ export function decideToolCall(toolName, runTrust, config = {}) {
|
||||
}
|
||||
|
||||
export function runKey(event = {}, ctx = {}) {
|
||||
const runId = typeof (event.runId || ctx.runId) === "string"
|
||||
? (event.runId || ctx.runId).trim()
|
||||
: "";
|
||||
const senderId = resolveSenderId(ctx);
|
||||
return runId && senderId ? JSON.stringify([runId, senderId]) : undefined;
|
||||
const exact = (...values) => {
|
||||
const normalized = values
|
||||
.filter((value) => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
return normalized.length > 0 && new Set(normalized).size === 1
|
||||
? normalized[0]
|
||||
: undefined;
|
||||
};
|
||||
const runId = exact(event.runId, ctx.runId);
|
||||
const agentId = exact(event.agentId, ctx.agentId);
|
||||
const sessionId = exact(
|
||||
event.sessionId,
|
||||
event.sessionKey,
|
||||
ctx.sessionId,
|
||||
ctx.sessionKey,
|
||||
);
|
||||
return runId && agentId && sessionId
|
||||
? JSON.stringify([runId, agentId, sessionId])
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
classifyRun,
|
||||
decideToolCall,
|
||||
resolveSenderId,
|
||||
runKey,
|
||||
} from "../dist/policy.js";
|
||||
|
||||
function registration(config = {}) {
|
||||
@@ -23,6 +24,24 @@ function registration(config = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function agentContext(id, senderId) {
|
||||
return {
|
||||
runId: `run-${id}`,
|
||||
agentId: "agent-main",
|
||||
sessionKey: `session-${id}`,
|
||||
...(senderId ? { senderId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function toolContext(id, extra = {}) {
|
||||
return {
|
||||
runId: `run-${id}`,
|
||||
agentId: "agent-main",
|
||||
sessionId: `session-${id}`,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
test("sender classification covers missing, blocked, allowlisted, and automation", () => {
|
||||
assert.equal(resolveSenderId({ channelContext: { sender: { id: " nested " } } }), "nested");
|
||||
assert.deepEqual(classifyRun({}, {}), {
|
||||
@@ -51,18 +70,18 @@ test("sender classification covers missing, blocked, allowlisted, and automation
|
||||
test("unknown agent runs default to pass but fail closed when explicitly enabled", async () => {
|
||||
const defaultHandlers = registration();
|
||||
assert.deepEqual(
|
||||
await defaultHandlers.beforeAgentRun({}, { runId: "r-default" }),
|
||||
await defaultHandlers.beforeAgentRun({}, agentContext("default")),
|
||||
{ outcome: "pass" },
|
||||
);
|
||||
|
||||
const enforced = registration({ blockUnknownAgentRuns: true });
|
||||
const denied = await enforced.beforeAgentRun({}, { runId: "r-denied" });
|
||||
const denied = await enforced.beforeAgentRun({}, agentContext("denied"));
|
||||
assert.equal(denied.outcome, "block");
|
||||
assert.equal(denied.reason, "sender_id_missing");
|
||||
assert.doesNotMatch(denied.reason + denied.message, /undefined|\r|\n/);
|
||||
|
||||
assert.deepEqual(
|
||||
await enforced.beforeAgentRun({}, { runId: "r-job", jobId: "job-1" }),
|
||||
await enforced.beforeAgentRun({}, { ...agentContext("job"), jobId: "job-1" }),
|
||||
{ outcome: "pass" },
|
||||
);
|
||||
});
|
||||
@@ -104,9 +123,9 @@ test("trusted human side effects request approval with exact timeout validation"
|
||||
};
|
||||
if (configured !== undefined) config.approvalTimeoutMs = configured;
|
||||
const handlers = registration(config);
|
||||
const ctx = { runId: `approval-${String(configured)}`, senderId: "alice" };
|
||||
await handlers.beforeAgentRun({}, ctx);
|
||||
const result = await handlers.beforeToolCall({ toolName: "shell" }, ctx);
|
||||
const id = `approval-${String(configured)}`;
|
||||
await handlers.beforeAgentRun({}, agentContext(id, "alice"));
|
||||
const result = await handlers.beforeToolCall({ toolName: "shell" }, toolContext(id));
|
||||
assert.equal(result.requireApproval.timeoutMs, expected, String(configured));
|
||||
assert.deepEqual(result.requireApproval.allowedDecisions, ["allow-once", "deny"]);
|
||||
}
|
||||
@@ -119,41 +138,41 @@ test("active run provenance is isolated by registration, run, and sender", async
|
||||
};
|
||||
const first = registration(config);
|
||||
const second = registration(config);
|
||||
await first.beforeAgentRun({}, { runId: "shared", senderId: "alice" });
|
||||
await first.beforeAgentRun({}, agentContext("shared", "alice"));
|
||||
|
||||
const otherSender = await first.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
{ runId: "shared", senderId: "bob" },
|
||||
toolContext("shared", { senderId: "bob" }),
|
||||
);
|
||||
assert.equal(otherSender.block, true);
|
||||
assert.match(otherSender.blockReason, /run_provenance_missing$/);
|
||||
assert.match(otherSender.blockReason, /run_scope_sender_conflict$/);
|
||||
|
||||
const otherRegistration = await second.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
{ runId: "shared", senderId: "alice" },
|
||||
toolContext("shared"),
|
||||
);
|
||||
assert.equal(otherRegistration.block, true);
|
||||
assert.match(otherRegistration.blockReason, /run_provenance_missing$/);
|
||||
});
|
||||
|
||||
test("agent_end cleanup removes only the matching run and sender", async () => {
|
||||
test("agent_end cleanup removes only the matching shared scope", async () => {
|
||||
const handlers = registration({
|
||||
requireExplicitTrustedSenders: true,
|
||||
trustedSenderIds: ["alice", "bob"],
|
||||
});
|
||||
await handlers.beforeAgentRun({}, { runId: "same", senderId: "alice" });
|
||||
await handlers.beforeAgentRun({}, { runId: "same", senderId: "bob" });
|
||||
await handlers.agentEnd({}, { runId: "same", senderId: "alice" });
|
||||
await handlers.beforeAgentRun({}, agentContext("alice-scope", "alice"));
|
||||
await handlers.beforeAgentRun({}, agentContext("bob-scope", "bob"));
|
||||
await handlers.agentEnd({}, toolContext("alice-scope"));
|
||||
|
||||
const alice = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
{ runId: "same", senderId: "alice" },
|
||||
toolContext("alice-scope"),
|
||||
);
|
||||
assert.equal(alice.block, true);
|
||||
|
||||
const bob = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
{ runId: "same", senderId: "bob" },
|
||||
toolContext("bob-scope"),
|
||||
);
|
||||
assert.ok(bob.requireApproval);
|
||||
});
|
||||
@@ -167,10 +186,76 @@ test("run provenance expires after the bounded age", async (t) => {
|
||||
requireExplicitTrustedSenders: true,
|
||||
trustedSenderIds: ["alice"],
|
||||
});
|
||||
const ctx = { runId: "expiring", senderId: "alice" };
|
||||
await handlers.beforeAgentRun({}, ctx);
|
||||
await handlers.beforeAgentRun({}, agentContext("expiring", "alice"));
|
||||
now += 60 * 60 * 1000 + 1;
|
||||
const result = await handlers.beforeToolCall({ toolName: "shell" }, ctx);
|
||||
const result = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
toolContext("expiring"),
|
||||
);
|
||||
assert.equal(result.block, true);
|
||||
assert.match(result.blockReason, /run_provenance_missing$/);
|
||||
});
|
||||
|
||||
test("host-shaped tool context without sender reuses trusted shared scope", async () => {
|
||||
const handlers = registration({
|
||||
requireExplicitTrustedSenders: true,
|
||||
trustedSenderIds: ["alice"],
|
||||
});
|
||||
await handlers.beforeAgentRun({}, agentContext("host-shape", "alice"));
|
||||
const result = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
toolContext("host-shape"),
|
||||
);
|
||||
assert.ok(result.requireApproval);
|
||||
assert.equal(result.block, undefined);
|
||||
});
|
||||
|
||||
test("shared scope collision fails closed without overwriting original sender", async () => {
|
||||
const handlers = registration({
|
||||
requireExplicitTrustedSenders: true,
|
||||
trustedSenderIds: ["alice", "bob"],
|
||||
});
|
||||
assert.deepEqual(
|
||||
await handlers.beforeAgentRun({}, agentContext("collision", "alice")),
|
||||
{ outcome: "pass" },
|
||||
);
|
||||
const collision = await handlers.beforeAgentRun(
|
||||
{},
|
||||
agentContext("collision", "bob"),
|
||||
);
|
||||
assert.equal(collision.outcome, "block");
|
||||
assert.equal(collision.reason, "run_scope_sender_conflict");
|
||||
assert.doesNotMatch(collision.message, /alice|bob/);
|
||||
|
||||
const original = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
toolContext("collision"),
|
||||
);
|
||||
assert.ok(original.requireApproval);
|
||||
});
|
||||
|
||||
test("shared scope normalization is exact and incomplete scope fails closed", async () => {
|
||||
const sessionId = runKey({}, {
|
||||
runId: " run-1 ", agentId: " agent-1 ", sessionId: " session-1 ",
|
||||
});
|
||||
const sessionKey = runKey({}, {
|
||||
runId: "run-1", agentId: "agent-1", sessionKey: "session-1",
|
||||
});
|
||||
assert.equal(sessionId, JSON.stringify(["run-1", "agent-1", "session-1"]));
|
||||
assert.equal(sessionKey, sessionId);
|
||||
assert.equal(runKey({}, {
|
||||
runId: "run-1",
|
||||
agentId: "agent-1",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "different-session",
|
||||
}), undefined);
|
||||
assert.equal(runKey({}, { agentId: "agent-1", sessionId: "session-1" }), undefined);
|
||||
|
||||
const handlers = registration();
|
||||
const missing = await handlers.beforeToolCall(
|
||||
{ toolName: "shell" },
|
||||
{ agentId: "agent-1", sessionId: "session-1" },
|
||||
);
|
||||
assert.equal(missing.block, true);
|
||||
assert.match(missing.blockReason, /run_provenance_missing$/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user