112 lines
3.3 KiB
JavaScript
112 lines
3.3 KiB
JavaScript
import {
|
|
classifyRun,
|
|
decideToolCall,
|
|
resolveSenderId,
|
|
runKey,
|
|
} from "./policy.js";
|
|
|
|
const MAX_RUN_AGE_MS = 60 * 60 * 1000;
|
|
|
|
function prune(activeRuns, now = Date.now()) {
|
|
for (const [key, value] of activeRuns.entries()) {
|
|
if (now - value.recordedAt > MAX_RUN_AGE_MS) activeRuns.delete(key);
|
|
}
|
|
}
|
|
|
|
function approvalTimeout(config) {
|
|
const value = config.approvalTimeoutMs;
|
|
return Number.isInteger(value) && value >= 1000 && value <= 300000
|
|
? value
|
|
: 60000;
|
|
}
|
|
|
|
export default {
|
|
id: "aios-tenant-boundary",
|
|
name: "AIOS Tenant Boundary",
|
|
description: "Fail-closed user-origin and side-effect policy for Tenant Cells.",
|
|
|
|
register(api) {
|
|
const activeRuns = new Map();
|
|
|
|
api.on(
|
|
"before_agent_run",
|
|
async (event, ctx) => {
|
|
prune(activeRuns);
|
|
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() });
|
|
}
|
|
|
|
if (
|
|
config.blockUnknownAgentRuns === true &&
|
|
trust.trusted !== true &&
|
|
trust.reason !== "job_id_present"
|
|
) {
|
|
return {
|
|
outcome: "block",
|
|
reason: trust.reason,
|
|
message: "This message cannot be verified as coming from the active authenticated user.",
|
|
};
|
|
}
|
|
return { outcome: "pass" };
|
|
},
|
|
{ priority: 100 },
|
|
);
|
|
|
|
api.on(
|
|
"before_tool_call",
|
|
async (event, ctx) => {
|
|
prune(activeRuns);
|
|
const config = event.context?.pluginConfig ?? api.pluginConfig ?? {};
|
|
const key = runKey(event, ctx);
|
|
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") {
|
|
return {
|
|
block: true,
|
|
blockReason: `AIOS V2R1 origin policy denied tool call: ${decision.code}`,
|
|
};
|
|
}
|
|
if (decision.action === "approval") {
|
|
return {
|
|
requireApproval: {
|
|
title: `Approve ${String(event.toolName)}`,
|
|
description: "Authenticated user requested a side-effecting tool call in this Tenant Cell.",
|
|
severity: "warning",
|
|
timeoutMs: approvalTimeout(config),
|
|
allowedDecisions: ["allow-once", "deny"],
|
|
pluginId: "aios-tenant-boundary",
|
|
},
|
|
};
|
|
}
|
|
return undefined;
|
|
},
|
|
{ priority: 100 },
|
|
);
|
|
|
|
api.on(
|
|
"agent_end",
|
|
async (event, ctx) => {
|
|
const key = runKey(event, ctx);
|
|
if (key) activeRuns.delete(key);
|
|
},
|
|
{ priority: 100 },
|
|
);
|
|
},
|
|
};
|