Keep external-agent feature reads on their admission transaction (#19750)

## Thinking Path

> - Paperclip manages work for AI agents.
> - External native agents must pass current feature and authority
checks.
> - The broker checks feature settings inside its admission transaction.
> - The old feature reader uses the root database pool.
> - Two active transactions can leave no connection for that read.
> - This change uses the admitted transaction for the same policy read.
> - It keeps the existing company, principal and controller checks.

## Linked Issues or Issue Description

Refs: #19340. That merged change fixes retained native Retry ownership.
This change leaves that recovery contract intact. Related: #18744 and
#17833 fix caller-owned transaction reads in other domains. Root
searched live GitHub for open externalBotEnabled changes and found no
duplicate.

**What happened?**

An external-agent admission can wait for another pool connection while
it holds a row lock. A second transaction can occupy the remaining
connection and wait for that lock. The feature read must use the
transaction that already owns admission.

**Expected behavior**

Dot, Grok and Muse read current feature settings on the admitted
transaction. Disabled providers remain disabled. Current authority
checks remain in force.

**Steps to reproduce**

1. Use a real PostgreSQL pool with two connections.
2. Lock the settings row in the admission transaction.
3. Occupy the second connection with a transaction that waits for that
row.
4. Read the existing provider feature policy through the admitted owner.
5. Require the read to finish before the competing lock is acquired.

**Paperclip version or commit**

The fix was tested at `d8dece250b75cffee67d3b7f4f0e669826282164`, based
on master `839d41aabbc6d042250548aedcaa5c50d369dca3`. It now includes
master `131b566ca4fa866c4348739c75cfe48c805b3398` through normal merge
`c2031b0fb3e5d1b34ebff1d5af8cc050d129bd9a`. All seven fix files retain
their tested blobs and modes.

## What Changed

- Pass the admitted select owner through the broker identity contract.
- Use that owner for Dot, Grok and Muse feature settings.
- Add a read-only settings projection that reuses the existing managed
and staging view. It does not create the settings row.
- Add six real PostgreSQL controls for enabled and disabled providers.
- Add three broker-path PostgreSQL regressions for exact transaction
forwarding through Dot, Grok and Muse.
- Keep all newer master broker changes and existing authority checks.

## Verification

Local controlled regression passed at tested revision
`d8dece250b75cffee67d3b7f4f0e669826282164`:

- Baseline: restore only the five original master production leaves.
Keep the same read-only settings projection and regression file. One
enabled-Dot case failed with the intended `57014`; it still read the
correct `true` policy. Five cases were unselected.
- The trace held the admission lock at 2.441 ms. The second connection
waited for that lock at 9.494 ms. The root-pool feature read finished at
2015.585 ms, after the competing statement timed out at 2014.982 ms.
- Candidate: all six enabled/disabled Dot, Grok and Muse controls
passed, with zero skips, in 14.528 s. Each read returned the configured
policy before the competing lock was acquired. Every competing
transaction committed without an error.
- Fresh normal candidate production declarations passed in 17.080 s. The
complete 615-root owning test program passed in 4.756 s. Own dependency
setup and TypeScript export builds had passed before these controls.
- Known worker groups and sockets closed. The single post-owner audit
found no other sessions in the test database. No forensic database was
changed.

An earlier selector attempt skipped all six cases and opened no test
database connection. It is not a regression pass or failure. Source
inspection proved that Vitest 5 filters names with ` > ` separators,
while its JSON reporter joins names with spaces. The selected control
then used the unique title-only pattern; its separate baseline failure
is preserved above.

The later normal master merge preserves all seven tested files. The 46
incoming paths change Native/workspace recovery, types and presentation.
They do not change the direct feature/settings read graph. The
direct-policy evidence therefore carries from its tested revision. It
was **not rerun at the merged revision**. Fresh merged-head checks at
`c2031b0fb3e5d1b34ebff1d5af8cc050d129bd9a` passed: the content-aware
Shared/SDK producer (1.325 s), normal Runner core and surfaces
TypeScript programs (0.936 s and 0.693 s), and the full normal App graph
(28.933 s). The App check included all production layers and all four
test programs: 616 normal test roots and 397/409/412 tree roots. Known
build owners and sockets closed. Database/Eval and provider asset
outputs remained unchanged. No install, Cargo, binary build, provider
execution or PG test repeated. Fresh final-head remote CI and review
remain **pending**.

The test entry is
`server/src/modules/external-agents/feature-reader-owner.postgres.test.ts`.
It requires a dedicated external
`PAPERCLIP_FEATURE_OWNER_TEST_DATABASE_URL` ending in `_test` and an
owned `PAPERCLIP_FEATURE_OWNER_TRACE` path. Its normal migration hook,
two-connection pool, case deadlines and assertions remain unchanged. No
provider or Runner binary is needed for these controls.

The review also identified a wiring coverage gap: the six contention
controls call the readers directly. A new whole-three file now traverses
the actual Dot/Grok/Muse broker factories, normal pending-pairing
producers, dispatch and real execution-admission transaction.
Pass-through observations compare the exact admission transaction with
the owner supplied to the normal readonly settings projection. Each
provider is disabled through normal settings and must return real 403,
read actual false policy, leave the queued run without Native authority,
and create no assignment. No grant, ready binding, policy result or
successful authorization is mocked.

With only the original five master production leaves restored, all three
new cases failed at the exact missing-owner assertion (`readOwners`
expected length 1, actual 0) after passing the real 403 and
admission-scope checks. The full baseline had0 pass / 3 fail / 0 skip in
12.691 s. With the exact candidate leaves restored, the unchanged
whole-three file passed 3/3 with zero skips in 10.101 s. Both variants
used actual Node 24 / Vitest 5 forks and a real PG pool of two; all
known owners/groups/sockets closed. The candidate's private temporary
directory was removed; the failed baseline database/home remain
retained. Root independently read all three raw baseline failures,
rehashed source and outputs, verified known owner/group closure, then
observed no remaining test sessions. The successful database was
normally removed; the original failing database and home remain
retained.

The new test entry is
`server/src/modules/external-agents/broker-feature-owner.postgres.test.ts`,
using dedicated `PAPERCLIP_BROKER_FEATURE_OWNER_TEST_DATABASE_URL`. The
complete normal 617-root owning test program passed in 4.883 s after a
normal exported config refresh; prior production declarations and
unrelated configs remained byte-identical. No compiler or
dependency/Rust work repeated for the PG variants. These results were
obtained with the seven production/source files at c203 plus the new
test; they are not claimed as a new published-head remote CI result. The
review repair is committed at
`d043860eb93a26c4416f184697a212f027d538e2`; it adds only this 117-line
test file to `c2031b0f`. Tracked production bytes are unchanged. Exact
published head `d043860eb93a26c4416f184697a212f027d538e2` now passes 56
checks with four expected skips. Greptile reviewed this exact head at
5/5 and confirms that the broker-test gap is addressed. The complete
review thread is resolved. Ponytail stopped without a verdict; one
normal retry was requested and no active turn or new finding was
observed. No earlier-head result is used as a new-head pass.

The controls prove feature policy and transaction ownership. They do not
prove deployed suspension, hosted Dot/Muse eligibility or full Native
task execution. The separate private useful-case pass does not supply
missing current-master or accounting evidence.

## Risks

The broker identity contract changes for all three providers. A missed
adapter could use the wrong owner. Current semantic, real-policy and
exact broker-owner identity tests cover that error.

The read-only projection reuses the normal settings view. It preserves
managed and staging rules. An absent row is projected without a write;
the existing initializing getter stays unchanged.

No schema, public API, queue, timer, rate limit or provider deadline
changes. No historical Retry or physical-stop proof is changed.

## Model Used

OpenAI `gpt-6.1-sol` assisted with source changes and tool use. An
OpenAI GPT-6 family coordinator reviewed the work; its exact runtime ID
was not exposed. Context-window sizes were unavailable.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (tested revision and
source-carry limits stated above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
user-facing command or contract change)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-07 01:02:03 -06:00
1 parent 6d112c74cf
commit 33ca62f776
8 files changed
+237 -7

No files matched your search

@@ -0,0 +1,117 @@
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
agents, applyPendingMigrations, authUsers, companies, companyMemberships, createDb,
dotAgentBindings, dotRunnerAssignments, heartbeatRuns, issues, museAgentBindings,
museRunnerAssignments, type Db,
} from "@paperclipai/db";
import { resolveExternalTestDatabaseUrl } from "../../lib/dedicated-test-database-url.js";
import { accessService } from "../../services/access.js";
import * as settings from "../../services/instance-settings.js";
import * as admission from "./admission.js";
import { dotRunnerBroker } from "./dot.js";
import { museExternalAgents } from "./muse.js";
const url = resolveExternalTestDatabaseUrl("PAPERCLIP_BROKER_FEATURE_OWNER_TEST_DATABASE_URL");
const providers = ["openai_dot", "grok_bot", "muse"] as const;
describe.skipIf(!url)("external broker forwards its admission owner to feature policy", () => {
let db: Db;
beforeAll(async () => { await applyPendingMigrations(url!); }, 90_000);
beforeEach(() => {
db = createDb(url!, { maxConnections: 2, applicationName: "broker-feature-owner" });
});
afterEach(async () => {
vi.restoreAllMocks();
await db.$client.end({ timeout: 5 });
}, 30_000);
for (const provider of providers) it(`${provider} reads disabled policy on the exact dispatch transaction`, async () => {
await settings.instanceSettingsService(db).updateExperimental({
enablePublicMcp: true, enableNativeRunner: true,
enableOpenAiDot: true, enableGrokBot: true, enableMuse: true,
});
const operatorId = randomUUID();
await db.insert(authUsers).values({
id: operatorId, name: "Feature owner operator", email: `${operatorId}@example.test`,
createdAt: new Date(), updatedAt: new Date(),
});
const [company] = await db.insert(companies).values({
name: "Broker feature owner", issuePrefix: `BF${operatorId.replaceAll("-", "")}`,
}).returning();
await db.insert(companyMemberships).values({
companyId: company.id, principalType: "user", principalId: operatorId,
membershipRole: "owner", status: "active",
});
await accessService(db).setPrincipalPermission(company.id, "user", operatorId, "agents:create", true, operatorId);
const [agent] = await db.insert(agents).values({
companyId: company.id, name: provider, adapterType: "paperclip_runner", status: "active",
adapterConfig: { provider, allowUnmeteredProvider: true },
}).returning();
const broker = provider === "muse"
? museExternalAgents(db, { publicOrigin: "https://paperclip.example" })
: dotRunnerBroker(db);
const pairingInput = { companyId: company.id, agentId: agent.id, operatorId };
const pairing = provider === "muse"
? await museExternalAgents(db, { publicOrigin: "https://paperclip.example" }).createPairing(pairingInput)
: await dotRunnerBroker(db).createPairing({ ...pairingInput, provider });
const [issue] = await db.insert(issues).values({ companyId: company.id, title: "Denied broker dispatch" }).returning();
const [run] = await db.insert(heartbeatRuns).values({
companyId: company.id, agentId: agent.id, issueId: issue.id, nativeIssueId: issue.id,
status: "queued", runtimeMode: "legacy",
}).returning();
await settings.instanceSettingsService(db).updateExperimental({
enableOpenAiDot: provider !== "openai_dot", enableGrokBot: provider !== "grok_bot",
enableMuse: provider !== "muse",
});
const admitted = vi.spyOn(admission, "withExternalExecutionAdmission");
const readOwners: Array<Pick<Db, "select"> | undefined> = [];
const readValues: boolean[] = [];
const originalSettings = settings.instanceSettingsService;
vi.spyOn(settings, "instanceSettingsService").mockImplementation((...args) => {
const service = originalSettings(...args);
return {
...service,
getExperimentalReadOnly: async (owner?: Pick<Db, "select">) => {
readOwners.push(owner);
const value = await service.getExperimentalReadOnly(owner);
readValues.push(provider === "openai_dot" ? value.enableOpenAiDot
: provider === "grok_bot" ? value.enableGrokBot : value.enableMuse);
return value;
},
};
});
const snapshot = {
companyId: company.id, agentId: agent.id, bindingId: pairing.bindingId, bindingGeneration: 1,
acceptByUnixMs: Date.now() + 60_000, expiresAtUnixMs: Date.now() + 120_000,
};
const binding = { companyId: company.id, agentId: agent.id, runId: run.id };
await expect(broker.port({ binding, provider: { binding: snapshot } }).dispatch({
sourceEventId: randomUUID(),
payload: {
kind: "assignment", binding: { ...binding, bindingId: snapshot.bindingId, bindingGeneration: snapshot.bindingGeneration,
normalizedSessionId: randomUUID(), turnId: "denied-feature", assignmentRevision: 1 },
tools: [], acceptByUnixMs: snapshot.acceptByUnixMs, expiresAtUnixMs: snapshot.expiresAtUnixMs,
},
})).rejects.toMatchObject({ status: 403, message: "External binding is unavailable for new effects." });
expect(admitted).toHaveBeenCalledTimes(1);
expect(admitted.mock.calls[0].slice(1, 4)).toEqual([company.id, agent.id, run.id]);
expect(readOwners).toHaveLength(1);
expect(readOwners[0]).toBe(admitted.mock.calls[0][0]);
expect(readOwners[0]).not.toBe(db);
expect(readValues).toEqual([false]);
expect(await db.select().from(dotRunnerAssignments).where(eq(dotRunnerAssignments.runId, run.id))).toEqual([]);
expect(await db.select().from(museRunnerAssignments).where(eq(museRunnerAssignments.runId, run.id))).toEqual([]);
const [unchanged] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id));
expect(unchanged).toMatchObject({ status: "queued", runtimeMode: "legacy", nativeSessionId: null });
if (provider === "muse") {
const [pending] = await db.select().from(museAgentBindings).where(eq(museAgentBindings.id, pairing.bindingId));
expect(pending).toMatchObject({ grantId: null, revokedAt: null });
} else {
const [pending] = await db.select().from(dotAgentBindings).where(eq(dotAgentBindings.id, pairing.bindingId));
expect(pending).toMatchObject({ status: "pairing", grantId: null, revokedAt: null });
}
}, 10_000);
});
+1 -1
View File
@@ -89,7 +89,7 @@ function createBroker(db: Db) {
return { binding, assignment: a };
}
const nativeBroker = createNativeExternalBroker<McpPrincipal>(db, "openai_dot", { enabled, authorize: authorizeAssignment });
const nativeBroker = createNativeExternalBroker<McpPrincipal>(db, "openai_dot", { enabled: (database, provider) => externalBotEnabled(db, provider ?? "openai_dot", database), authorize: authorizeAssignment });
return {
enabled,
@@ -0,0 +1,100 @@
import {appendFile} from "node:fs/promises";
import {performance} from "node:perf_hooks";
import {eq,sql} from "drizzle-orm";
import {afterEach,beforeAll,beforeEach,describe,expect,it} from "vitest";
import {applyPendingMigrations,createDb,instanceSettings,type Db} from "@paperclipai/db";
import {resolveExternalTestDatabaseUrl} from "../../lib/dedicated-test-database-url.js";
import {externalBotEnabled} from "../../services/external-bot-authority.js";
import {instanceSettingsService} from "../../services/instance-settings.js";
import {museIdentity} from "../../services/muse-identity.js";
const url=resolveExternalTestDatabaseUrl("PAPERCLIP_FEATURE_OWNER_TEST_DATABASE_URL");
type QueryOwner=Pick<Db,"select">;
type Provider="openai_dot"|"grok_bot"|"muse";
const cases=[
{provider:"openai_dot",enabled:true},
{provider:"openai_dot",enabled:false},
{provider:"grok_bot",enabled:true},
{provider:"grok_bot",enabled:false},
{provider:"muse",enabled:true},
{provider:"muse",enabled:false},
] satisfies Array<{provider:Provider;enabled:boolean}>;
const states=new Set(["57014","40P01","55P03","08006","25P02"]);
function sqlState(error:unknown){
let current=error;
for(let depth=0;depth<3&&current&&typeof current==="object";depth++){
if("code"in current&&typeof current.code==="string"&&states.has(current.code))return current.code;
current="cause"in current?current.cause:undefined;
}
return "other";
}
describe.skipIf(!url)("existing external feature readers keep the admitted query owner",()=>{
let db:Db;
beforeAll(async()=>{await applyPendingMigrations(url!);},90_000);
beforeEach(()=>{db=createDb(url!,{maxConnections:2,applicationName:"feature-owner-pool2"});});
afterEach(async()=>{await db.$client.end({timeout:5});},30_000);
for(const item of cases)it(`reads ${item.provider} enabled=${item.enabled} with both pool2 slots owned`,async()=>{
const artifact=process.env.PAPERCLIP_FEATURE_OWNER_TRACE;
if(!artifact)throw new Error("Explicit owned feature trace path required");
const row=await instanceSettingsService(db).updateExperimental({
enablePublicMcp:true,enableNativeRunner:true,
enableOpenAiDot:item.provider==="openai_dot"?item.enabled:true,
enableGrokBot:item.provider==="grok_bot"?item.enabled:true,
enableMuse:item.provider==="muse"?item.enabled:true,
});
const started=performance.now(),events:Array<{phase:string;atMs:number}>=[];
function mark(phase:string){
if(events.length>=16)throw new Error("Feature owner trace overflow");
events.push({phase,atMs:performance.now()-started});
console.log(JSON.stringify({kind:"feature-owner-phase",provider:item.provider,enabled:item.enabled,phase}));
}
let competing:Promise<void>|undefined,competingError:string|null=null,observed:boolean|undefined;
try{
await db.transaction(async tx=>{
mark("admission-transaction-entered");
expect(await tx.select({id:instanceSettings.id}).from(instanceSettings).where(eq(instanceSettings.id,row.id)).for("update")).toHaveLength(1);
mark("admission-row-locked");
let occupiedResolve!:()=>void,occupiedReject!:(error:unknown)=>void;
const occupied=new Promise<void>((resolve,reject)=>{occupiedResolve=resolve;occupiedReject=reject;});
mark("competing-connection-requested");
competing=db.transaction(async other=>{
mark("competing-transaction-entered");
await other.execute(sql`set local statement_timeout = '2000ms'`);
mark("competing-timeout-installed");
await other.execute(sql`select pg_backend_pid()`);
mark("second-connection-owned");occupiedResolve();
mark("competing-row-lock-requested");
expect(await other.select({id:instanceSettings.id}).from(instanceSettings).where(eq(instanceSettings.id,row.id)).for("update")).toHaveLength(1);
mark("competing-row-lock-acquired");
}).then(()=>{mark("competing-committed");},error=>{
competingError=sqlState(error);mark("competing-failed");occupiedReject(error);
});
await occupied;
mark("feature-read-start");
if(item.provider==="muse"){
const read:(owner:QueryOwner)=>Promise<boolean>=museIdentity(db).enabled;
observed=await read(tx);
}else{
const read:(root:Db,provider:"openai_dot"|"grok_bot",owner:QueryOwner)=>Promise<boolean>=externalBotEnabled;
observed=await read(db,item.provider,tx);
}
mark("feature-read-finished");
});
mark("admission-committed");await competing;
expect(competingError,JSON.stringify({events,competingError,observed})).toBeNull();
expect(observed).toBe(item.enabled);
const names=events.map(event=>event.phase);
expect(names.indexOf("feature-read-finished")).toBeLessThan(names.indexOf("competing-row-lock-acquired"));
expect(names).toContain("competing-committed");
expect(await instanceSettingsService(db).getExperimentalReadOnly()).toMatchObject({
enableOpenAiDot:item.provider==="openai_dot"?item.enabled:true,
enableGrokBot:item.provider==="grok_bot"?item.enabled:true,
enableMuse:item.provider==="muse"?item.enabled:true,
});
}finally{
await competing;
await appendFile(artifact,JSON.stringify({kind:"feature-owner-result",provider:item.provider,enabled:item.enabled,events,competingError,observed})+"\n",{mode:0o600});
}
},10_000);
});
+1 -1
View File
@@ -133,7 +133,7 @@ function createMuseBroker(db:Db, publicOrigin?:string) {
await logActivity(tx as unknown as Db,{companyId:b.companyId,actorType,actorId:operatorId,action:"muse.binding_revoked",entityType:"agent",entityId:b.agentId,details:{bindingId:b.id,generation:b.generation,authorizingUserId:b.operatorId,source:reason,externalStopConfirmed:false}});
return runs;
}
const nativeBroker=createNativeExternalBroker<AgentConnectionSubject>(db,"muse",{enabled:identity.enabled,expectedResource:connectionResource,
const nativeBroker=createNativeExternalBroker<AgentConnectionSubject>(db,"muse",{enabled:database=>identity.enabled(database),expectedResource:connectionResource,
assertSubjectCredential:async(tx,s)=>{await assertMuseSubjectCredential(tx,s,s.credentialSource==="oauth"?connectionResource():undefined);},
authorize:async(s,id,settled)=>{const {b,a}=await authorizeAssignment(s,id,settled);return {binding:b,assignment:a};}});
const operation=nativeBroker.operation;
@@ -31,7 +31,7 @@ interface Receipt extends Record<string,unknown> {id:string;companyId:string;ass
interface Authority {binding:{id:string;companyId:string;agentId:string;generation:number};assignment:Pick<Assignment,"id"|"companyId"|"agentId"|"bindingId"|"bindingGeneration"|"runId"|"normalizedSessionId"|"turnId"|"revision"|"controllerGeneration"|"status"> & {expiresAt:Date;createdAt:Date;claimedAt?:Date|null}}
interface Identity<S> {
authorize(subject:S,assignmentId:string,settled?:boolean):Promise<Authority>;
enabled(provider?:"openai_dot"|"grok_bot"):Promise<boolean>;
enabled(database:Pick<Db,"select">,provider?:"openai_dot"|"grok_bot"):Promise<boolean>;
expectedResource?():string;
assertSubjectCredential?(tx:ExternalAdmissionTransaction,subject:S):Promise<void>;
}
@@ -60,7 +60,7 @@ export function createNativeExternalBroker<S>(db:Db,provider:Provider,identity:I
async function assertCurrentAuthority(database:Database,b:Binding,allowFence=false) {
if(allowFence)return;
const actualProvider=provider==="muse"?"muse":b.providerKind;
if(!actualProvider||!["openai_dot","grok_bot","muse"].includes(actualProvider)||!await identity.enabled(actualProvider==="muse"?undefined:actualProvider)||b.status!=="ready"||b.revokedAt)throw forbidden("External binding is unavailable for new effects.");
if(!actualProvider||!["openai_dot","grok_bot","muse"].includes(actualProvider)||!await identity.enabled(database,actualProvider==="muse"?undefined:actualProvider)||b.status!=="ready"||b.revokedAt)throw forbidden("External binding is unavailable for new effects.");
const [agent]=await database.select().from(agents).where(and(eq(agents.id,b.agentId),eq(agents.companyId,b.companyId)));
const [company]=await database.select().from(companies).where(eq(companies.id,b.companyId));
const [user]=await database.select({id:authUsers.id}).from(authUsers).where(eq(authUsers.id,b.operatorId));
@@ -6,8 +6,11 @@ import { boardAuthService } from "./board-auth.js";
import { instanceSettingsService } from "./instance-settings.js";
import { McpOAuthError, type McpPrincipal } from "./public-mcp/oauth.js";
export async function externalBotEnabled(db: Db, provider: "openai_dot" | "grok_bot") {
const experimental = await instanceSettingsService(db).getExperimental();
export async function externalBotEnabled(db: Db, provider: "openai_dot" | "grok_bot", queryOwner?: Pick<Db, "select">) {
const settings = instanceSettingsService(db);
const experimental = queryOwner
? await settings.getExperimentalReadOnly(queryOwner)
: await settings.getExperimental();
return experimental.enablePublicMcp && (provider === "openai_dot" ? experimental.enableOpenAiDot : experimental.enableGrokBot && experimental.enableNativeRunner);
}
+6
View File
@@ -583,6 +583,12 @@ export function instanceSettingsService(db: Db, options: InstanceSettingsService
return toGeneralView(row.general);
},
getExperimentalReadOnly: async (runner: Pick<Db, "select"> = db): Promise<InstanceExperimentalSettingsWithManaged> => {
const [row] = await runner.select({ experimental: instanceSettings.experimental }).from(instanceSettings)
.where(eq(instanceSettings.singletonKey, DEFAULT_SINGLETON_KEY));
return toExperimentalView(row?.experimental);
},
getExperimental: async (): Promise<InstanceExperimentalSettingsWithManaged> => {
const row = await getOrCreateRow();
return toExperimentalView(row.experimental);
+5 -1
View File
@@ -156,7 +156,11 @@ export async function assertMuseCleanupCredential(db:MuseIdentityDatabase,s:Agen
}
export function museIdentity(db: Db) {
async function enabled() { const s=await instanceSettingsService(db).getExperimental(); return s.enableMuse && s.enableNativeRunner; }
async function enabled(queryOwner?: Pick<Db, "select">) {
const settings = instanceSettingsService(db);
const s = queryOwner ? await settings.getExperimentalReadOnly(queryOwner) : await settings.getExperimental();
return s.enableMuse && s.enableNativeRunner;
}
async function issue(tx: ExternalAdmissionTransaction, b: typeof bindings.$inferSelect, familyId: string=randomUUID()): Promise<MuseCredentials> {
const accessToken=token(), refreshToken=token(), cleanupToken=token(), detectorCleanupToken=token();
const accessExpiresAt=new Date(Date.now()+MUSE_ACCESS_TTL_MS), refreshExpiresAt=new Date(Date.now()+MUSE_REFRESH_INACTIVITY_MS);