Scope provider requests to workspaces

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 22:16:37 -04:00
1 parent ee5eff7725
commit 335d57b77f
15 files changed
+144 -16

No files matched your search

@@ -23,7 +23,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
connection.close();
});
@@ -149,7 +149,7 @@ describe("migration service", () => {
create table chat_presence(principal_type text, principal_id text);
create table chat_read_state(org_id text, employee_id text, space_id text, last_read_message_id text);
create table providers(id text primary key, org_id text, credential_id text);
create table provider_requests(id text primary key, org_id text, provider_id text);
create table provider_requests(id text primary key, org_id text, provider_id text, workspace_id text);
create table provider_usage_windows(id text primary key, org_id text, provider_id text);
create table provider_rate_limit_observations(id text primary key, org_id text, provider_id text);
create table credentials(id text primary key, org_id text);
@@ -225,7 +225,7 @@ describe("migration service", () => {
insert into board_interventions(id, org_id, thread_id, message_id, approval_id) values ('bdi-1', 'org-1', 'thread-1', 'msg-1', 'approval-1');
insert into chat_presence(principal_type, principal_id) values ('agent', 'agent-1');
insert into chat_read_state(org_id, employee_id, space_id, last_read_message_id) values ('org-1', 'emp-1', 'space-1', 'msg-1');
insert into provider_requests(id, org_id, provider_id) values ('req-1', 'org-1', 'prov-1');
insert into provider_requests(id, org_id, provider_id, workspace_id) values ('req-1', 'org-1', 'prov-1', 'wsp-1');
insert into provider_usage_windows(id, org_id, provider_id) values ('usage-1', 'org-1', 'prov-1');
insert into provider_rate_limit_observations(id, org_id, provider_id) values ('quota-1', 'org-1', 'prov-1');
insert into skill_definitions(id, org_id) values ('skill-1', 'org-1');
@@ -142,11 +142,11 @@ describe("provider service streaming", () => {
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider", model: "gpt-5.4" });
connection.db.prepare(`
insert into provider_requests(
id, org_id, provider_id, provider_key, model, prompt_sha256,
id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256,
response_text, raw_response_json, state, error_message, input_char_count,
output_char_count, execution_mode, policy_decision, external_response_id,
stream_event_count, cancelled_at, created_at, completed_at
) values (?, ?, ?, 'openai', 'gpt-5.4', ?, 'partial', '[]', ?, null, 6, 7, ?, 'allowed:standard', ?, 2, null, ?, null)
) values (?, ?, ?, 'openai', null, 'gpt-5.4', ?, 'partial', '[]', ?, null, 6, 7, ?, 'allowed:standard:organization', ?, 2, null, ?, null)
`).run(
requestId,
orgId,
@@ -380,7 +380,7 @@ describe("provider service", () => {
inputCharCount: 16,
outputCharCount: 25,
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
policyDecision: "allowed:standard",
policyDecision: "allowed:standard:organization",
});
expect(state.providerRateLimits.find((item) => item.providerId === openAiProviderId)).toMatchObject({
providerId: openAiProviderId,
@@ -394,6 +394,68 @@ describe("provider service", () => {
connection.close();
});
test("requires workspace context for workspace-scoped provider execution", async () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const adapters: ProviderAdapterRegistry = {
...providerAdapters,
openai: {
...providerAdapters.openai,
parseAuthProfile: (secret) => ({ type: "api_key", apiKey: secret.trim() }),
checkHealth: async () => ({ checkedAt: new Date().toISOString(), rateLimitState: null }),
createResponse: async ({ prompt }) => ({ text: `accepted:${prompt}`, rawResponseJson: "{}", rateLimitState: null }),
},
};
const providers = new ProviderService(connection.db, adapters, testVault);
let orgId = "";
let openAiProviderId = "";
let workspaceId = "";
connection.transaction(() => {
orgId = createOrganization(organization);
openAiProviderId = providerId(connection, orgId, "openai");
workspaceId = "wsp_release";
const now = new Date().toISOString();
connection.db.prepare(`
insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, null, ?, ?, ?)
`).run(workspaceId, orgId, "Release", "C:/work/release", "https://example.com/release.git", "main", 1, now, now);
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider", model: "gpt-5.4" });
providers.updatePolicy({
orgId,
providerId: openAiProviderId,
dataPolicy: PROVIDER_DATA_POLICY.STANDARD,
backgroundEnabled: false,
monthlyRequestLimit: 10,
workspaceScope: "workspace",
});
});
await expect(providers.createResponse({
orgId,
providerId: openAiProviderId,
prompt: "Draft workspace memo",
instructions: null,
})).rejects.toThrow("Provider workspace scope requires a workspace context.");
expect(connection.db.prepare("select count(*) as count from provider_requests").get()).toEqual({ count: 0 });
await providers.createResponse({
orgId,
providerId: openAiProviderId,
workspaceId,
prompt: "Draft workspace memo",
instructions: null,
});
expect(snapshot.load().providerRequests[0]).toMatchObject({
providerId: openAiProviderId,
workspaceId,
policyDecision: `allowed:standard:workspace:${workspaceId}`,
});
connection.close();
});
test("blocks background execution under zero data retention policy", async () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
@@ -50,6 +50,7 @@ describe("task execution service", () => {
let orgId = "";
let providerId = "";
let taskId = "";
let workspaceId = "";
connection.transaction(() => {
orgId = organization.createOrganization({
@@ -60,6 +61,12 @@ describe("task execution service", () => {
});
providerId = openAiProviderId(connection, orgId);
providers.saveCredential({ orgId, providerId, secret: "sk-live-provider", model: "gpt-5.4" });
workspaceId = "wsp_task_execution";
const now = new Date().toISOString();
connection.db.prepare(`
insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, null, ?, ?, ?)
`).run(workspaceId, orgId, "Task Workspace", "C:/work/task", "https://example.com/task.git", "main", 1, now, now);
const agentId = organization.createAgent({
orgId,
name: "Ada",
@@ -79,7 +86,7 @@ describe("task execution service", () => {
risk: RISK_LEVEL.MEDIUM,
projectId: null,
goalId: null,
workspaceId: null,
workspaceId,
priority: TASK_PRIORITY.HIGH,
requiresFilesystem: false,
});
@@ -91,9 +98,10 @@ describe("task execution service", () => {
const request = state.providerRequests.find((item) => item.id === run?.providerRequestId);
expect(run).toMatchObject({ taskId, agentId: expect.any(String), providerId, state: "completed", errorMessage: null });
expect(request).toMatchObject({ providerId, state: "succeeded", responseText: "executed:gpt-5.4" });
expect(request).toMatchObject({ providerId, workspaceId, state: "succeeded", responseText: "executed:gpt-5.4" });
expect(capturedPrompt).toContain("Task: Draft launch checklist");
expect(capturedPrompt).toContain("Assignee: Ada (Execution lead)");
expect(capturedPrompt).toContain("Workspace: Task Workspace");
expect(state.auditEvents.some((event) => event.entityType === "task_run" && event.action === "completed")).toBe(true);
connection.close();
});