@standardagents/builder 0.29.2 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/built-in-routes.js +188 -2
- package/dist/built-in-routes.js.map +1 -1
- package/dist/index.js +692 -17
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +66 -5
- package/dist/plugin.js.map +1 -1
- package/dist/runtime.d.ts +54 -2
- package/dist/runtime.js +626 -12
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProviderModelPricing } from '@standardagents/provider-pricing';
|
|
2
2
|
import { jsonPathReader } from '@formkit/jsonreader';
|
|
3
|
-
import { ProviderError, mapReasoningLevel, THREAD_STREAM_REASONING_PARAM } from '@standardagents/spec';
|
|
3
|
+
import { ProviderError, mapReasoningLevel, THREAD_STREAM_REASONING_PARAM, isUserEndpoint } from '@standardagents/spec';
|
|
4
4
|
export { ProviderError, belongsToPackage, defineAgent, defineHook, defineModel, definePrompt, defineProvider, defineTool, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
5
5
|
import 'virtual:@standardagents-providers';
|
|
6
6
|
import { z } from 'zod';
|
|
@@ -14098,10 +14098,35 @@ var init_ThreadStateImpl = __esm({
|
|
|
14098
14098
|
return { parentThreadId, parentStub };
|
|
14099
14099
|
}
|
|
14100
14100
|
_getAgentBuilderStub() {
|
|
14101
|
-
const env = this.
|
|
14101
|
+
const env = this._getRuntimeEnv();
|
|
14102
14102
|
const agentBuilderId = env.AGENT_BUILDER.idFromName("singleton");
|
|
14103
14103
|
return env.AGENT_BUILDER.get(agentBuilderId);
|
|
14104
14104
|
}
|
|
14105
|
+
/**
|
|
14106
|
+
* Environment bindings for this state. In-DO construction uses the thread
|
|
14107
|
+
* instance's own env; stub-backed states (thread endpoints, the user
|
|
14108
|
+
* scope's `thread()`) have no `instance.env` over RPC and use the env the
|
|
14109
|
+
* dispatcher recorded in the runtime context instead.
|
|
14110
|
+
*/
|
|
14111
|
+
_getRuntimeEnv() {
|
|
14112
|
+
const instanceEnv = this._threadInstance.env;
|
|
14113
|
+
const env = instanceEnv?.AGENT_BUILDER ? instanceEnv : this._runtimeContext.env;
|
|
14114
|
+
if (!env?.AGENT_BUILDER) {
|
|
14115
|
+
throw new Error(
|
|
14116
|
+
"No runtime environment available on this ThreadState. Stub-backed states must be constructed with runtimeContext.env."
|
|
14117
|
+
);
|
|
14118
|
+
}
|
|
14119
|
+
return env;
|
|
14120
|
+
}
|
|
14121
|
+
/** The per-user hub stub for this thread's user (user-scope surfaces). */
|
|
14122
|
+
_getUserHubStub(userId) {
|
|
14123
|
+
const env = this._getRuntimeEnv();
|
|
14124
|
+
const namespace = env.AGENT_BUILDER_USER_HUB;
|
|
14125
|
+
if (!namespace) {
|
|
14126
|
+
throw new Error("User scope unavailable: AGENT_BUILDER_USER_HUB binding is missing.");
|
|
14127
|
+
}
|
|
14128
|
+
return namespace.get(namespace.idFromName(userId));
|
|
14129
|
+
}
|
|
14105
14130
|
// ─────────────────────────────────────────────────────────────────────────
|
|
14106
14131
|
// Skills — instance-global Agent Skills library, backed by the
|
|
14107
14132
|
// DurableAgentBuilder singleton (skills are never stored per-thread or on
|
|
@@ -14118,7 +14143,67 @@ var init_ThreadStateImpl = __esm({
|
|
|
14118
14143
|
const stub = this._getAgentBuilderStub();
|
|
14119
14144
|
const user = await stub.getUserById(userId).catch(() => null);
|
|
14120
14145
|
if (!user) return null;
|
|
14146
|
+
const inUserScope = this._runtimeContext.inUserScope === true;
|
|
14147
|
+
const agentId = this._metadata.agent_id;
|
|
14148
|
+
const hub = () => this._getUserHubStub(userId);
|
|
14149
|
+
const capabilities = {
|
|
14150
|
+
call: async (endpoint, payload) => {
|
|
14151
|
+
if (inUserScope) {
|
|
14152
|
+
throw new Error(
|
|
14153
|
+
"Reentrant user-scope call: this ThreadState was raised from user-scope code; state.user().call() is unavailable here."
|
|
14154
|
+
);
|
|
14155
|
+
}
|
|
14156
|
+
const path20 = endpoint.replace(/^\/+/, "");
|
|
14157
|
+
return hub().fetch(
|
|
14158
|
+
new Request(`https://user-hub/users/${encodeURIComponent(userId)}/endpoints/${path20}`, {
|
|
14159
|
+
method: "POST",
|
|
14160
|
+
headers: {
|
|
14161
|
+
"Content-Type": "application/json",
|
|
14162
|
+
// Runtime-internal caller channel — the public routes overwrite
|
|
14163
|
+
// this header, so only thread-scope code can present "thread".
|
|
14164
|
+
"X-Standard-Agents-Caller": "thread"
|
|
14165
|
+
},
|
|
14166
|
+
body: payload === void 0 ? void 0 : JSON.stringify(payload)
|
|
14167
|
+
})
|
|
14168
|
+
);
|
|
14169
|
+
},
|
|
14170
|
+
getValue: async (key) => {
|
|
14171
|
+
return await stub.getUserValue?.(userId, key);
|
|
14172
|
+
},
|
|
14173
|
+
setValue: async (key, value) => {
|
|
14174
|
+
await stub.setUserValue?.(userId, key, value);
|
|
14175
|
+
},
|
|
14176
|
+
emit: (event, data) => {
|
|
14177
|
+
const delivery = hub().broadcastUserEvent(userId, event, data).catch(() => {
|
|
14178
|
+
});
|
|
14179
|
+
this._pendingEventDeliveries.add(delivery);
|
|
14180
|
+
void delivery.finally(
|
|
14181
|
+
() => this._pendingEventDeliveries.delete(delivery)
|
|
14182
|
+
);
|
|
14183
|
+
},
|
|
14184
|
+
fetch: async (grant, input, init) => {
|
|
14185
|
+
const body = init?.body;
|
|
14186
|
+
if (body !== void 0 && body !== null && typeof body !== "string") {
|
|
14187
|
+
throw new Error("Grant fetch bodies must be strings in this runtime.");
|
|
14188
|
+
}
|
|
14189
|
+
return hub().fetch(
|
|
14190
|
+
new Request(`https://user-hub/users/${encodeURIComponent(userId)}/grant-fetch`, {
|
|
14191
|
+
method: "POST",
|
|
14192
|
+
headers: { "Content-Type": "application/json" },
|
|
14193
|
+
body: JSON.stringify({
|
|
14194
|
+
grant,
|
|
14195
|
+
url: String(input),
|
|
14196
|
+
method: init?.method ?? "GET",
|
|
14197
|
+
headers: init?.headers ? Object.fromEntries(new Headers(init.headers)) : {},
|
|
14198
|
+
body: body ?? null,
|
|
14199
|
+
caller: { kind: "agent", name: agentId }
|
|
14200
|
+
})
|
|
14201
|
+
})
|
|
14202
|
+
);
|
|
14203
|
+
}
|
|
14204
|
+
};
|
|
14121
14205
|
return {
|
|
14206
|
+
...capabilities,
|
|
14122
14207
|
id: user.id,
|
|
14123
14208
|
username: user.username,
|
|
14124
14209
|
role: user.role,
|
|
@@ -15476,8 +15561,9 @@ function createHmrReloader() {
|
|
|
15476
15561
|
const isPromptFile = file.startsWith(dirs.promptsDir) && file.endsWith(".ts");
|
|
15477
15562
|
const isAgentFile = file.startsWith(dirs.agentsDir) && file.endsWith(".ts");
|
|
15478
15563
|
const isEffectFile = file.startsWith(dirs.effectsDir) && file.endsWith(".ts");
|
|
15564
|
+
const isUserEndpointFile = file.startsWith(dirs.usersDir) && file.endsWith(".ts");
|
|
15479
15565
|
const isConfigFile = isModelFile || isProviderFile || isPromptFile || isAgentFile || isEffectFile;
|
|
15480
|
-
if (!isToolFile && !isThreadApiFile && !isHookFile && !isConfigFile) return;
|
|
15566
|
+
if (!isToolFile && !isThreadApiFile && !isHookFile && !isConfigFile && !isUserEndpointFile) return;
|
|
15481
15567
|
reloader.invalidateSourceFileModule(server, file);
|
|
15482
15568
|
if (isConfigFile || isToolFile) regenerateTypes();
|
|
15483
15569
|
if (isToolFile) await reloader.reloadToolsModule(server);
|
|
@@ -17503,6 +17589,35 @@ function parseRequestBody(req) {
|
|
|
17503
17589
|
req.on("error", reject);
|
|
17504
17590
|
});
|
|
17505
17591
|
}
|
|
17592
|
+
function scanUsersDirectory(dir) {
|
|
17593
|
+
const results = [];
|
|
17594
|
+
if (!fs8__default.existsSync(dir)) return results;
|
|
17595
|
+
const walk = (current, prefix) => {
|
|
17596
|
+
let entries;
|
|
17597
|
+
try {
|
|
17598
|
+
entries = fs8__default.readdirSync(current, { withFileTypes: true });
|
|
17599
|
+
} catch (error) {
|
|
17600
|
+
console.error(`Error scanning users directory ${current}:`, error);
|
|
17601
|
+
return;
|
|
17602
|
+
}
|
|
17603
|
+
for (const entry of entries) {
|
|
17604
|
+
const fullPath = path8__default.join(current, entry.name);
|
|
17605
|
+
if (entry.isDirectory()) {
|
|
17606
|
+
walk(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
17607
|
+
continue;
|
|
17608
|
+
}
|
|
17609
|
+
if (!entry.name.endsWith(".ts") || entry.name.endsWith(".d.ts")) continue;
|
|
17610
|
+
const base = entry.name.slice(0, -3);
|
|
17611
|
+
const key = prefix ? `${prefix}/${base}` : base;
|
|
17612
|
+
results.push({
|
|
17613
|
+
key,
|
|
17614
|
+
importPath: "./" + path8__default.relative(process.cwd(), fullPath).replace(/\\/g, "/")
|
|
17615
|
+
});
|
|
17616
|
+
}
|
|
17617
|
+
};
|
|
17618
|
+
walk(dir, "");
|
|
17619
|
+
return results;
|
|
17620
|
+
}
|
|
17506
17621
|
|
|
17507
17622
|
// src/plugin/agent-api-middleware.ts
|
|
17508
17623
|
async function handleAgentApiRequest(context) {
|
|
@@ -21100,9 +21215,10 @@ function createDevMiddleware(server, context) {
|
|
|
21100
21215
|
});
|
|
21101
21216
|
}
|
|
21102
21217
|
async function loadBuilderVirtualModule(context) {
|
|
21103
|
-
const { toolsDir, hooksDir, modelsDir, promptsDir, agentsDir, effectsDir, hmr } = context;
|
|
21218
|
+
const { toolsDir, hooksDir, modelsDir, promptsDir, agentsDir, effectsDir, usersDir, hmr } = context;
|
|
21104
21219
|
const tools = await scanToolsDirectory(toolsDir);
|
|
21105
21220
|
const hooks = await scanHooksDirectory(hooksDir);
|
|
21221
|
+
const userEndpoints = scanUsersDirectory(usersDir);
|
|
21106
21222
|
const models = await scanModelsDirectory(modelsDir);
|
|
21107
21223
|
const prompts = await scanPromptsDirectory(promptsDir);
|
|
21108
21224
|
const agents = await scanAgentsDirectory(agentsDir);
|
|
@@ -21204,6 +21320,17 @@ async function loadBuilderVirtualModule(context) {
|
|
|
21204
21320
|
},`;
|
|
21205
21321
|
}
|
|
21206
21322
|
}).join("\n");
|
|
21323
|
+
const userEndpointsCode = userEndpoints.map(({ key, importPath }) => {
|
|
21324
|
+
const absPath = toAbsolutePath(importPath);
|
|
21325
|
+
return ` "${key}": async () => {
|
|
21326
|
+
try {
|
|
21327
|
+
return (await import("${absPath}")).default;
|
|
21328
|
+
} catch (error) {
|
|
21329
|
+
console.error('Failed to import user endpoint ${key}:', error);
|
|
21330
|
+
throw error;
|
|
21331
|
+
}
|
|
21332
|
+
},`;
|
|
21333
|
+
}).join("\n");
|
|
21207
21334
|
return `// Consolidated virtual module: virtual:@standardagents/builder /* v${hmr.versions.builder} */
|
|
21208
21335
|
// Provides DurableThread, DurableAgentBuilder, ThreadWatchdog, and router
|
|
21209
21336
|
import { DurableThread as _BaseDurableThread } from '@standardagents/builder/runtime';
|
|
@@ -21249,6 +21376,10 @@ const _effects = {
|
|
|
21249
21376
|
${effectsCode}
|
|
21250
21377
|
};
|
|
21251
21378
|
|
|
21379
|
+
const _userEndpoints = {
|
|
21380
|
+
${userEndpointsCode}
|
|
21381
|
+
};
|
|
21382
|
+
|
|
21252
21383
|
/**
|
|
21253
21384
|
* DurableThread with all virtual module methods already implemented.
|
|
21254
21385
|
* Exported by the generated AgentBuilder Worker entry; user projects do not
|
|
@@ -21338,10 +21469,21 @@ export class CodeExecutionBridge extends _BaseCodeExecutionBridge {}
|
|
|
21338
21469
|
export class ThreadWatchdog extends _BaseThreadWatchdog {}
|
|
21339
21470
|
|
|
21340
21471
|
/**
|
|
21341
|
-
* Per-user hub companion
|
|
21342
|
-
*
|
|
21472
|
+
* Per-user hub companion: persistent daemon socket, wake signal, sharded
|
|
21473
|
+
* account KV \u2014 and the USER SCOPE (spec: Users): packaged user endpoints from
|
|
21474
|
+
* the users/ directory executed under per-user serialization, the user event
|
|
21475
|
+
* stream, and user lifecycle hooks (filtered from the shared hooks registry
|
|
21476
|
+
* by hook type at dispatch).
|
|
21343
21477
|
*/
|
|
21344
|
-
export class DurableUserHub extends _BaseDurableUserHub {
|
|
21478
|
+
export class DurableUserHub extends _BaseDurableUserHub {
|
|
21479
|
+
userEndpoints() {
|
|
21480
|
+
return _userEndpoints;
|
|
21481
|
+
}
|
|
21482
|
+
|
|
21483
|
+
userHooks() {
|
|
21484
|
+
return _hooks;
|
|
21485
|
+
}
|
|
21486
|
+
}
|
|
21345
21487
|
`;
|
|
21346
21488
|
}
|
|
21347
21489
|
async function loadRegistryVirtualModule(context) {
|
|
@@ -22397,6 +22539,7 @@ function agentbuilder(options = {}) {
|
|
|
22397
22539
|
const promptsDir = options.promptsDir ? path8__default.resolve(process.cwd(), options.promptsDir) : path8__default.resolve(process.cwd(), "agents/prompts");
|
|
22398
22540
|
const agentsDir = options.agentsDir ? path8__default.resolve(process.cwd(), options.agentsDir) : path8__default.resolve(process.cwd(), "agents/agents");
|
|
22399
22541
|
const effectsDir = options.effectsDir ? path8__default.resolve(process.cwd(), options.effectsDir) : path8__default.resolve(process.cwd(), "agents/effects");
|
|
22542
|
+
const usersDir = options.usersDir ? path8__default.resolve(process.cwd(), options.usersDir) : path8__default.resolve(process.cwd(), "agents/users");
|
|
22400
22543
|
const outputDir = path8__default.resolve(process.cwd(), ".agents");
|
|
22401
22544
|
const generatedWorkerEntry = path8__default.join(outputDir, "worker.ts");
|
|
22402
22545
|
const isProviderPackageInstalled = (packageName) => {
|
|
@@ -22459,6 +22602,7 @@ export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog
|
|
|
22459
22602
|
toolsDir,
|
|
22460
22603
|
hooksDir,
|
|
22461
22604
|
threadApiDir,
|
|
22605
|
+
usersDir,
|
|
22462
22606
|
modelsDir,
|
|
22463
22607
|
providersDir,
|
|
22464
22608
|
promptsDir,
|
|
@@ -22469,6 +22613,7 @@ export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog
|
|
|
22469
22613
|
toolsDir,
|
|
22470
22614
|
hooksDir,
|
|
22471
22615
|
threadApiDir,
|
|
22616
|
+
usersDir,
|
|
22472
22617
|
modelsDir,
|
|
22473
22618
|
providersDir,
|
|
22474
22619
|
promptsDir,
|
|
@@ -22556,6 +22701,7 @@ export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, ThreadWatchdog
|
|
|
22556
22701
|
this.addWatchFile(promptsDir);
|
|
22557
22702
|
this.addWatchFile(agentsDir);
|
|
22558
22703
|
this.addWatchFile(effectsDir);
|
|
22704
|
+
this.addWatchFile(usersDir);
|
|
22559
22705
|
},
|
|
22560
22706
|
configureServer(server) {
|
|
22561
22707
|
createDevMiddleware(server, devMiddlewareContext);
|
|
@@ -22660,7 +22806,9 @@ function createThreadEndpointHandler(handler) {
|
|
|
22660
22806
|
const durableId = env.AGENT_BUILDER_THREAD.idFromName(threadId);
|
|
22661
22807
|
const stub = env.AGENT_BUILDER_THREAD.get(durableId);
|
|
22662
22808
|
const instance = stub;
|
|
22663
|
-
const threadState = ThreadStateImpl.fromThreadInstance(instance, metadata
|
|
22809
|
+
const threadState = ThreadStateImpl.fromThreadInstance(instance, metadata, {
|
|
22810
|
+
env
|
|
22811
|
+
});
|
|
22664
22812
|
const endpointParams = {};
|
|
22665
22813
|
for (const [key, value] of Object.entries(params)) {
|
|
22666
22814
|
if (key === "threadId" || key === "id" && !params.threadId) {
|
|
@@ -34540,6 +34688,232 @@ function formatMs(ms) {
|
|
|
34540
34688
|
const minutes = Math.floor(seconds / 60);
|
|
34541
34689
|
return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
34542
34690
|
}
|
|
34691
|
+
|
|
34692
|
+
// src/users/route-match.ts
|
|
34693
|
+
var METHOD_SUFFIXES = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete"]);
|
|
34694
|
+
function parseUserRouteKey(key) {
|
|
34695
|
+
const parts = key.split("/");
|
|
34696
|
+
let file = parts.pop() ?? "";
|
|
34697
|
+
let method = "GET";
|
|
34698
|
+
const dot = file.lastIndexOf(".");
|
|
34699
|
+
if (dot >= 0) {
|
|
34700
|
+
const suffix = file.slice(dot + 1).toLowerCase();
|
|
34701
|
+
if (METHOD_SUFFIXES.has(suffix)) {
|
|
34702
|
+
method = suffix.toUpperCase();
|
|
34703
|
+
file = file.slice(0, dot);
|
|
34704
|
+
}
|
|
34705
|
+
}
|
|
34706
|
+
const segments = [...parts];
|
|
34707
|
+
if (file !== "index") segments.push(file);
|
|
34708
|
+
let dynamism = 0;
|
|
34709
|
+
for (const segment of segments) {
|
|
34710
|
+
if (segment === "[*]") dynamism = 2;
|
|
34711
|
+
else if (dynamism < 2 && segment.startsWith("[") && segment.endsWith("]")) dynamism = 1;
|
|
34712
|
+
}
|
|
34713
|
+
return { key, method, segments, dynamism };
|
|
34714
|
+
}
|
|
34715
|
+
function matchSegments(route, path20) {
|
|
34716
|
+
const params = {};
|
|
34717
|
+
for (let i = 0; i < route.length; i++) {
|
|
34718
|
+
const seg = route[i];
|
|
34719
|
+
if (seg === "[*]") {
|
|
34720
|
+
if (i !== route.length - 1) return null;
|
|
34721
|
+
params["*"] = path20.slice(i).join("/");
|
|
34722
|
+
return params;
|
|
34723
|
+
}
|
|
34724
|
+
const value = path20[i];
|
|
34725
|
+
if (value === void 0) return null;
|
|
34726
|
+
if (seg.startsWith("[") && seg.endsWith("]")) {
|
|
34727
|
+
params[seg.slice(1, -1)] = value;
|
|
34728
|
+
} else if (seg !== value) {
|
|
34729
|
+
return null;
|
|
34730
|
+
}
|
|
34731
|
+
}
|
|
34732
|
+
return path20.length === route.length ? params : null;
|
|
34733
|
+
}
|
|
34734
|
+
function matchUserRoute(keys, method, endpointPath) {
|
|
34735
|
+
const path20 = endpointPath.split("/").filter((s) => s.length > 0);
|
|
34736
|
+
const wanted = method.toUpperCase();
|
|
34737
|
+
let best = null;
|
|
34738
|
+
for (const key of keys) {
|
|
34739
|
+
const route = parseUserRouteKey(key);
|
|
34740
|
+
if (route.method !== wanted) continue;
|
|
34741
|
+
const params = matchSegments(route.segments, path20);
|
|
34742
|
+
if (!params) continue;
|
|
34743
|
+
if (!best || route.dynamism < best.route.dynamism) {
|
|
34744
|
+
best = { route, params };
|
|
34745
|
+
}
|
|
34746
|
+
}
|
|
34747
|
+
return best ? { key: best.route.key, params: best.params } : null;
|
|
34748
|
+
}
|
|
34749
|
+
|
|
34750
|
+
// src/users/UserStateImpl.ts
|
|
34751
|
+
init_ThreadStateImpl();
|
|
34752
|
+
var UserStateImpl = class {
|
|
34753
|
+
userId;
|
|
34754
|
+
host;
|
|
34755
|
+
/** Caller descriptor for grant `allow` policy checks. */
|
|
34756
|
+
caller;
|
|
34757
|
+
constructor(userId, host, caller) {
|
|
34758
|
+
this.userId = userId;
|
|
34759
|
+
this.host = host;
|
|
34760
|
+
this.caller = caller;
|
|
34761
|
+
}
|
|
34762
|
+
singleton() {
|
|
34763
|
+
const ns = this.host.env.AGENT_BUILDER;
|
|
34764
|
+
if (!ns) throw new Error("AGENT_BUILDER binding unavailable in the user scope.");
|
|
34765
|
+
return ns.get(ns.idFromName("singleton"));
|
|
34766
|
+
}
|
|
34767
|
+
async identity() {
|
|
34768
|
+
const user = await this.singleton().getUserById(this.userId);
|
|
34769
|
+
if (!user) throw new Error(`Unknown user: ${this.userId}`);
|
|
34770
|
+
return {
|
|
34771
|
+
id: user.id,
|
|
34772
|
+
username: user.username,
|
|
34773
|
+
role: user.role,
|
|
34774
|
+
permissions: user.permissions ?? null,
|
|
34775
|
+
email: user.email ?? null,
|
|
34776
|
+
display_name: user.display_name ?? null,
|
|
34777
|
+
avatar_url: user.avatar_url ?? null
|
|
34778
|
+
};
|
|
34779
|
+
}
|
|
34780
|
+
async getValue(key) {
|
|
34781
|
+
return await this.host.getUserValue(this.userId, key);
|
|
34782
|
+
}
|
|
34783
|
+
async setValue(key, value) {
|
|
34784
|
+
await this.host.setUserValue(this.userId, key, value);
|
|
34785
|
+
}
|
|
34786
|
+
async listValues(options) {
|
|
34787
|
+
const { entries, total } = await this.host.listUserValues(this.userId, options);
|
|
34788
|
+
return { entries, total };
|
|
34789
|
+
}
|
|
34790
|
+
async env(name) {
|
|
34791
|
+
const values = await this.singleton().getUserEnv(this.userId);
|
|
34792
|
+
const value = values[name];
|
|
34793
|
+
if (value === void 0) throw new Error(`Environment variable not found: ${name}`);
|
|
34794
|
+
return value;
|
|
34795
|
+
}
|
|
34796
|
+
emit(event, data) {
|
|
34797
|
+
this.host.broadcastUserEvent(this.userId, event, data);
|
|
34798
|
+
}
|
|
34799
|
+
async listThreads(options) {
|
|
34800
|
+
const { threads, total } = await this.singleton().listThreads({
|
|
34801
|
+
user_id: this.userId,
|
|
34802
|
+
agent_name: options?.agentId,
|
|
34803
|
+
limit: options?.limit,
|
|
34804
|
+
offset: options?.offset
|
|
34805
|
+
});
|
|
34806
|
+
return {
|
|
34807
|
+
threads: threads.map((t) => ({
|
|
34808
|
+
id: t.id,
|
|
34809
|
+
agent_id: t.agent_id ?? t.agent_name ?? "",
|
|
34810
|
+
tags: t.tags ?? [],
|
|
34811
|
+
created_at: t.created_at
|
|
34812
|
+
})),
|
|
34813
|
+
total
|
|
34814
|
+
};
|
|
34815
|
+
}
|
|
34816
|
+
async fetch(grant, input, init) {
|
|
34817
|
+
const body = init?.body;
|
|
34818
|
+
if (body !== void 0 && body !== null && typeof body !== "string") {
|
|
34819
|
+
throw new Error("Grant fetch bodies must be strings in this runtime.");
|
|
34820
|
+
}
|
|
34821
|
+
return this.host.grantFetch(this.userId, grant, {
|
|
34822
|
+
url: String(input),
|
|
34823
|
+
method: init?.method ?? "GET",
|
|
34824
|
+
headers: init?.headers ? Object.fromEntries(new Headers(init.headers)) : {},
|
|
34825
|
+
body: body ?? null,
|
|
34826
|
+
caller: this.caller
|
|
34827
|
+
});
|
|
34828
|
+
}
|
|
34829
|
+
/**
|
|
34830
|
+
* Raise one of this user's threads at rest — the same stub-backed
|
|
34831
|
+
* materialization thread endpoints receive. `inUserScope` marks the state
|
|
34832
|
+
* so a reentrant `state.user().call()` fails instead of deadlocking the
|
|
34833
|
+
* serialized user execution (spec: Users §2.2.3).
|
|
34834
|
+
*/
|
|
34835
|
+
async thread(threadId) {
|
|
34836
|
+
const metadata = await this.singleton().getThread(threadId);
|
|
34837
|
+
if (!metadata) throw new Error(`Thread not found: ${threadId}`);
|
|
34838
|
+
if (metadata.user_id !== this.userId) {
|
|
34839
|
+
throw new Error(`Thread ${threadId} does not belong to user ${this.userId}.`);
|
|
34840
|
+
}
|
|
34841
|
+
const meta = metadata;
|
|
34842
|
+
if (!meta.agent_id && meta.agent_name) meta.agent_id = meta.agent_name;
|
|
34843
|
+
const ns = this.host.env.AGENT_BUILDER_THREAD;
|
|
34844
|
+
if (!ns) throw new Error("AGENT_BUILDER_THREAD binding unavailable in the user scope.");
|
|
34845
|
+
const instance = ns.get(ns.idFromName(threadId));
|
|
34846
|
+
return ThreadStateImpl.fromThreadInstance(instance, meta, {
|
|
34847
|
+
env: this.host.env,
|
|
34848
|
+
inUserScope: true
|
|
34849
|
+
});
|
|
34850
|
+
}
|
|
34851
|
+
};
|
|
34852
|
+
|
|
34853
|
+
// src/users/serialize.ts
|
|
34854
|
+
function createExecutionChain() {
|
|
34855
|
+
let tail2 = Promise.resolve();
|
|
34856
|
+
return {
|
|
34857
|
+
run(fn) {
|
|
34858
|
+
const next = tail2.then(fn, fn);
|
|
34859
|
+
tail2 = next.catch(() => {
|
|
34860
|
+
});
|
|
34861
|
+
return next;
|
|
34862
|
+
}
|
|
34863
|
+
};
|
|
34864
|
+
}
|
|
34865
|
+
|
|
34866
|
+
// src/users/grants.ts
|
|
34867
|
+
function originAllowed(origins, target) {
|
|
34868
|
+
if (!Array.isArray(origins)) return false;
|
|
34869
|
+
for (const allowed of origins) {
|
|
34870
|
+
if (allowed.startsWith("*.")) {
|
|
34871
|
+
const suffix = allowed.slice(1);
|
|
34872
|
+
if (target.protocol === "https:" && target.hostname.endsWith(suffix)) return true;
|
|
34873
|
+
} else {
|
|
34874
|
+
try {
|
|
34875
|
+
if (new URL(allowed).origin === target.origin) return true;
|
|
34876
|
+
} catch {
|
|
34877
|
+
}
|
|
34878
|
+
}
|
|
34879
|
+
}
|
|
34880
|
+
return false;
|
|
34881
|
+
}
|
|
34882
|
+
function callerAllowed(allow, caller) {
|
|
34883
|
+
if (!allow) return false;
|
|
34884
|
+
const lists = {
|
|
34885
|
+
tool: allow.tools,
|
|
34886
|
+
endpoint: allow.endpoints,
|
|
34887
|
+
userEndpoint: allow.userEndpoints,
|
|
34888
|
+
userHook: allow.userEndpoints,
|
|
34889
|
+
agent: allow.agents
|
|
34890
|
+
};
|
|
34891
|
+
const list = lists[caller.kind];
|
|
34892
|
+
if (!list) return false;
|
|
34893
|
+
const bare = caller.name.replace(/\.(get|post|put|patch|delete)$/i, "");
|
|
34894
|
+
return list.includes(caller.name) || list.includes(bare);
|
|
34895
|
+
}
|
|
34896
|
+
function injectGrantCredential(config, secret, target, headers) {
|
|
34897
|
+
const format = config.inject?.format ?? "SECRET";
|
|
34898
|
+
switch (config.kind) {
|
|
34899
|
+
case "bearer":
|
|
34900
|
+
headers.set("Authorization", `Bearer ${secret}`);
|
|
34901
|
+
return true;
|
|
34902
|
+
case "header":
|
|
34903
|
+
headers.set(config.inject?.header ?? "Authorization", format.replace(/SECRET/g, secret));
|
|
34904
|
+
return true;
|
|
34905
|
+
case "query":
|
|
34906
|
+
target.searchParams.set(config.inject?.query ?? "key", secret);
|
|
34907
|
+
return true;
|
|
34908
|
+
case "basic":
|
|
34909
|
+
headers.set("Authorization", `Basic ${btoa(secret)}`);
|
|
34910
|
+
return true;
|
|
34911
|
+
default:
|
|
34912
|
+
return false;
|
|
34913
|
+
}
|
|
34914
|
+
}
|
|
34915
|
+
|
|
34916
|
+
// src/durable-objects/DurableUserHub.ts
|
|
34543
34917
|
var DurableUserHub = class extends DurableObject {
|
|
34544
34918
|
/** Persistent client (daemon) sockets, restored across hibernation. */
|
|
34545
34919
|
clientSockets = /* @__PURE__ */ new Set();
|
|
@@ -34547,11 +34921,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34547
34921
|
migrated = false;
|
|
34548
34922
|
constructor(ctx, env) {
|
|
34549
34923
|
super(ctx, env);
|
|
34550
|
-
|
|
34551
|
-
|
|
34552
|
-
|
|
34553
|
-
);
|
|
34554
|
-
}
|
|
34924
|
+
ctx.blockConcurrencyWhile(async () => {
|
|
34925
|
+
this.ensureMigrated();
|
|
34926
|
+
});
|
|
34555
34927
|
for (const ws of ctx.getWebSockets()) {
|
|
34556
34928
|
const attachment = ws.deserializeAttachment();
|
|
34557
34929
|
if (attachment?.type === "hub_client") {
|
|
@@ -34579,8 +34951,38 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34579
34951
|
this.ctx.storage.sql.exec(
|
|
34580
34952
|
`CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)`
|
|
34581
34953
|
);
|
|
34954
|
+
this.ctx.storage.sql.exec(`
|
|
34955
|
+
CREATE TABLE IF NOT EXISTS user_grants (
|
|
34956
|
+
user_id TEXT NOT NULL,
|
|
34957
|
+
name TEXT NOT NULL,
|
|
34958
|
+
secret TEXT NOT NULL,
|
|
34959
|
+
config TEXT NOT NULL,
|
|
34960
|
+
created_at INTEGER NOT NULL,
|
|
34961
|
+
updated_at INTEGER NOT NULL,
|
|
34962
|
+
PRIMARY KEY (user_id, name)
|
|
34963
|
+
)
|
|
34964
|
+
`);
|
|
34582
34965
|
this.migrated = true;
|
|
34583
34966
|
}
|
|
34967
|
+
// ─── User scope: registries (overridden by the generated virtual module) ──
|
|
34968
|
+
/**
|
|
34969
|
+
* Returns the user endpoints registry for lazy-loading `users/` files.
|
|
34970
|
+
* Keys are endpoint file paths relative to `users/` with the extension
|
|
34971
|
+
* stripped except the method suffix (e.g. `standardcode/lease_acquire.post`).
|
|
34972
|
+
* Implemented when DurableUserHub is imported from
|
|
34973
|
+
* 'virtual:@standardagents/builder'.
|
|
34974
|
+
*/
|
|
34975
|
+
userEndpoints() {
|
|
34976
|
+
return {};
|
|
34977
|
+
}
|
|
34978
|
+
/**
|
|
34979
|
+
* Returns the hooks registry (same registry the thread runtime uses). The
|
|
34980
|
+
* hub filters it to user-scope hook types at dispatch time. Implemented
|
|
34981
|
+
* when DurableUserHub is imported from 'virtual:@standardagents/builder'.
|
|
34982
|
+
*/
|
|
34983
|
+
userHooks() {
|
|
34984
|
+
return {};
|
|
34985
|
+
}
|
|
34584
34986
|
// ─── Persistent client socket ──────────────────────────────────────────────
|
|
34585
34987
|
/**
|
|
34586
34988
|
* Accept the user's persistent client (daemon) WebSocket. The daemon holds
|
|
@@ -34589,6 +34991,16 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34589
34991
|
*/
|
|
34590
34992
|
async fetch(request) {
|
|
34591
34993
|
const url = new URL(request.url);
|
|
34994
|
+
const scopeMatch = /^\/users\/([^/]+)\/(stream|endpoints|grant-fetch)(?:\/(.*))?$/.exec(
|
|
34995
|
+
url.pathname
|
|
34996
|
+
);
|
|
34997
|
+
if (scopeMatch) {
|
|
34998
|
+
const userId = decodeURIComponent(scopeMatch[1]);
|
|
34999
|
+
const kind = scopeMatch[2];
|
|
35000
|
+
if (kind === "stream") return this.acceptUserStream(request, url, userId);
|
|
35001
|
+
if (kind === "grant-fetch") return this.handleGrantFetch(request, userId);
|
|
35002
|
+
return this.runUserEndpoint(request, userId, scopeMatch[3] ?? "");
|
|
35003
|
+
}
|
|
34592
35004
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
34593
35005
|
const pair = new WebSocketPair();
|
|
34594
35006
|
const [client, server] = [pair[0], pair[1]];
|
|
@@ -34597,6 +35009,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34597
35009
|
clientName: url.searchParams.get("client_name"),
|
|
34598
35010
|
clientKind: url.searchParams.get("client_kind")
|
|
34599
35011
|
};
|
|
35012
|
+
this.ensureHeartbeatAutoResponse();
|
|
34600
35013
|
this.ctx.acceptWebSocket(server);
|
|
34601
35014
|
server.serializeAttachment({ type: "hub_client", ...info });
|
|
34602
35015
|
this.clientSockets.add(server);
|
|
@@ -34609,16 +35022,265 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34609
35022
|
}
|
|
34610
35023
|
return new Response("Not found", { status: 404 });
|
|
34611
35024
|
}
|
|
34612
|
-
|
|
34613
|
-
|
|
35025
|
+
// ─── User scope: event stream (spec: Users §4) ─────────────────────────────
|
|
35026
|
+
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
35027
|
+
* Registered at accept time rather than in the constructor so a hub that
|
|
35028
|
+
* never accepts a socket stays fully evictable (this also keeps
|
|
35029
|
+
* vitest-pool-workers' isolated storage able to pop the DO's frame). */
|
|
35030
|
+
ensureHeartbeatAutoResponse() {
|
|
35031
|
+
if (typeof WebSocketRequestResponsePair !== "undefined") {
|
|
35032
|
+
this.ctx.setWebSocketAutoResponse(
|
|
35033
|
+
new WebSocketRequestResponsePair("stream_ping", "stream_pong")
|
|
35034
|
+
);
|
|
35035
|
+
}
|
|
35036
|
+
}
|
|
35037
|
+
/** Sockets attached to the user event stream, restored after hibernation. */
|
|
35038
|
+
streamSockets() {
|
|
35039
|
+
return this.ctx.getWebSockets().filter((ws) => ws.deserializeAttachment()?.type === "user_stream");
|
|
35040
|
+
}
|
|
35041
|
+
acceptUserStream(request, url, userId) {
|
|
35042
|
+
if (request.headers.get("Upgrade") !== "websocket") {
|
|
35043
|
+
return Response.json({ error: "WebSocket upgrade required" }, { status: 426 });
|
|
35044
|
+
}
|
|
35045
|
+
const pair = new WebSocketPair();
|
|
35046
|
+
const [client, server] = [pair[0], pair[1]];
|
|
35047
|
+
const clientId = url.searchParams.get("client_id") ?? crypto.randomUUID();
|
|
35048
|
+
const clientKind = url.searchParams.get("client_kind");
|
|
35049
|
+
this.ensureHeartbeatAutoResponse();
|
|
35050
|
+
this.ctx.acceptWebSocket(server);
|
|
35051
|
+
server.serializeAttachment({ type: "user_stream", userId, clientId, clientKind });
|
|
35052
|
+
try {
|
|
35053
|
+
server.send(JSON.stringify({ event: "stream_ready", data: { client_id: clientId } }));
|
|
35054
|
+
} catch {
|
|
35055
|
+
}
|
|
35056
|
+
this.dispatchUserHooks(userId, "user_client_attached", [
|
|
35057
|
+
{ id: clientId, kind: clientKind ?? void 0 }
|
|
35058
|
+
]);
|
|
35059
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
35060
|
+
}
|
|
35061
|
+
/**
|
|
35062
|
+
* Deliver an event frame to every socket attached to this user's stream.
|
|
35063
|
+
* At-most-once, live-only (spec: Users §4) — no history, no acks.
|
|
35064
|
+
*/
|
|
35065
|
+
broadcastUserEvent(userId, event, data) {
|
|
35066
|
+
const frame = JSON.stringify({ event, data });
|
|
35067
|
+
for (const ws of this.streamSockets()) {
|
|
35068
|
+
const attachment = ws.deserializeAttachment();
|
|
35069
|
+
if (attachment?.userId !== userId) continue;
|
|
35070
|
+
try {
|
|
35071
|
+
ws.send(frame);
|
|
35072
|
+
} catch {
|
|
35073
|
+
}
|
|
35074
|
+
}
|
|
35075
|
+
}
|
|
35076
|
+
// ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
|
|
35077
|
+
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
35078
|
+
executionChain = createExecutionChain();
|
|
35079
|
+
scopeHost() {
|
|
35080
|
+
return {
|
|
35081
|
+
getUserValue: (userId, key) => this.getUserValue(userId, key),
|
|
35082
|
+
setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
|
|
35083
|
+
listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
|
|
35084
|
+
broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
|
|
35085
|
+
grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
|
|
35086
|
+
env: this.env
|
|
35087
|
+
};
|
|
35088
|
+
}
|
|
35089
|
+
async runUserEndpoint(request, userId, endpointPath) {
|
|
35090
|
+
const registry = this.userEndpoints();
|
|
35091
|
+
const match = matchUserRoute(Object.keys(registry), request.method, endpointPath);
|
|
35092
|
+
if (!match) return Response.json({ error: "Not found" }, { status: 404 });
|
|
35093
|
+
let loaded;
|
|
35094
|
+
try {
|
|
35095
|
+
loaded = await registry[match.key]();
|
|
35096
|
+
} catch (error) {
|
|
35097
|
+
return Response.json(
|
|
35098
|
+
{ error: `Failed to load user endpoint: ${match.key}` },
|
|
35099
|
+
{ status: 500 }
|
|
35100
|
+
);
|
|
35101
|
+
}
|
|
35102
|
+
const module = loaded;
|
|
35103
|
+
const handler = module.default ?? loaded;
|
|
35104
|
+
if (!isUserEndpoint(handler)) {
|
|
35105
|
+
return Response.json(
|
|
35106
|
+
{ error: `${match.key} is not a defineUserEndpoint export` },
|
|
35107
|
+
{ status: 500 }
|
|
35108
|
+
);
|
|
35109
|
+
}
|
|
35110
|
+
return this.executionChain.run(async () => {
|
|
35111
|
+
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
35112
|
+
kind: "userEndpoint",
|
|
35113
|
+
name: match.key
|
|
35114
|
+
});
|
|
35115
|
+
try {
|
|
35116
|
+
return await handler.__handler(request, user, match.params);
|
|
35117
|
+
} catch (error) {
|
|
35118
|
+
return Response.json(
|
|
35119
|
+
{ error: error instanceof Error ? error.message : "User endpoint failed" },
|
|
35120
|
+
{ status: 500 }
|
|
35121
|
+
);
|
|
35122
|
+
}
|
|
35123
|
+
});
|
|
35124
|
+
}
|
|
35125
|
+
// ─── User scope: lifecycle hooks (spec: Users §7) ──────────────────────────
|
|
35126
|
+
/**
|
|
35127
|
+
* Fire every definition of a user hook type. Observational and best-effort:
|
|
35128
|
+
* failures are swallowed and dispatch never blocks the triggering
|
|
35129
|
+
* operation. Nested dispatch is suppressed so a `user_value_changed` hook
|
|
35130
|
+
* writing values cannot recurse.
|
|
35131
|
+
*/
|
|
35132
|
+
inUserHookDispatch = false;
|
|
35133
|
+
dispatchUserHooks(userId, hookType, args) {
|
|
35134
|
+
if (this.inUserHookDispatch) return;
|
|
35135
|
+
const registry = this.userHooks();
|
|
35136
|
+
const names = Object.keys(registry);
|
|
35137
|
+
if (names.length === 0) return;
|
|
35138
|
+
const run = async () => {
|
|
35139
|
+
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
35140
|
+
kind: "userHook",
|
|
35141
|
+
name: hookType
|
|
35142
|
+
});
|
|
35143
|
+
for (const name of names) {
|
|
35144
|
+
try {
|
|
35145
|
+
const loaded = await registry[name]();
|
|
35146
|
+
const definition = loaded.default ?? loaded;
|
|
35147
|
+
if (definition?.hook !== hookType || typeof definition.execute !== "function") {
|
|
35148
|
+
continue;
|
|
35149
|
+
}
|
|
35150
|
+
this.inUserHookDispatch = true;
|
|
35151
|
+
try {
|
|
35152
|
+
await definition.execute(user, ...args);
|
|
35153
|
+
} finally {
|
|
35154
|
+
this.inUserHookDispatch = false;
|
|
35155
|
+
}
|
|
35156
|
+
} catch {
|
|
35157
|
+
}
|
|
35158
|
+
}
|
|
35159
|
+
};
|
|
35160
|
+
this.ctx.waitUntil(run());
|
|
34614
35161
|
}
|
|
34615
35162
|
async webSocketClose(ws) {
|
|
35163
|
+
const attachment = ws.deserializeAttachment();
|
|
35164
|
+
if (attachment?.type === "user_stream") {
|
|
35165
|
+
this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
|
|
35166
|
+
{
|
|
35167
|
+
id: typeof attachment.clientId === "string" ? attachment.clientId : "unknown",
|
|
35168
|
+
kind: typeof attachment.clientKind === "string" ? attachment.clientKind : void 0
|
|
35169
|
+
}
|
|
35170
|
+
]);
|
|
35171
|
+
return;
|
|
35172
|
+
}
|
|
34616
35173
|
this.clientSockets.delete(ws);
|
|
34617
35174
|
this.clientInfo.delete(ws);
|
|
34618
35175
|
}
|
|
35176
|
+
// ─── User scope: credential grants (spec: Users §6) ───────────────────────
|
|
35177
|
+
/**
|
|
35178
|
+
* Store or replace a grant. `secret` is write-only from this point on.
|
|
35179
|
+
* Policy (`CredentialGrant` minus the secret) is kept as JSON config.
|
|
35180
|
+
*/
|
|
35181
|
+
async setGrant(userId, grant, secret) {
|
|
35182
|
+
this.ensureMigrated();
|
|
35183
|
+
const { name, ...config } = grant;
|
|
35184
|
+
const now = Date.now();
|
|
35185
|
+
await this.ctx.storage.sql.exec(
|
|
35186
|
+
`INSERT INTO user_grants (user_id, name, secret, config, created_at, updated_at)
|
|
35187
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?5)
|
|
35188
|
+
ON CONFLICT(user_id, name) DO UPDATE SET
|
|
35189
|
+
secret = excluded.secret, config = excluded.config, updated_at = excluded.updated_at`,
|
|
35190
|
+
userId,
|
|
35191
|
+
name,
|
|
35192
|
+
secret,
|
|
35193
|
+
JSON.stringify(config),
|
|
35194
|
+
now
|
|
35195
|
+
);
|
|
35196
|
+
}
|
|
35197
|
+
async deleteGrant(userId, name) {
|
|
35198
|
+
this.ensureMigrated();
|
|
35199
|
+
await this.ctx.storage.sql.exec(
|
|
35200
|
+
`DELETE FROM user_grants WHERE user_id = ? AND name = ?`,
|
|
35201
|
+
userId,
|
|
35202
|
+
name
|
|
35203
|
+
);
|
|
35204
|
+
}
|
|
35205
|
+
/** Metadata-only listing — never returns secret material. */
|
|
35206
|
+
async listGrants(userId) {
|
|
35207
|
+
this.ensureMigrated();
|
|
35208
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
35209
|
+
`SELECT name, config FROM user_grants WHERE user_id = ? ORDER BY name ASC`,
|
|
35210
|
+
userId
|
|
35211
|
+
);
|
|
35212
|
+
return cursor.toArray().map((row) => {
|
|
35213
|
+
let config = {};
|
|
35214
|
+
try {
|
|
35215
|
+
config = JSON.parse(row.config);
|
|
35216
|
+
} catch {
|
|
35217
|
+
}
|
|
35218
|
+
return { name: row.name, ...config };
|
|
35219
|
+
});
|
|
35220
|
+
}
|
|
35221
|
+
/** Perform a privileged fetch with the grant's credential injected. */
|
|
35222
|
+
async grantFetch(userId, grantName, req) {
|
|
35223
|
+
this.ensureMigrated();
|
|
35224
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
35225
|
+
`SELECT secret, config FROM user_grants WHERE user_id = ? AND name = ? LIMIT 1`,
|
|
35226
|
+
userId,
|
|
35227
|
+
grantName
|
|
35228
|
+
);
|
|
35229
|
+
const row = cursor.toArray()[0];
|
|
35230
|
+
if (!row) {
|
|
35231
|
+
return Response.json({ error: "grant_not_found", grant: grantName }, { status: 404 });
|
|
35232
|
+
}
|
|
35233
|
+
let config;
|
|
35234
|
+
try {
|
|
35235
|
+
config = JSON.parse(row.config);
|
|
35236
|
+
} catch {
|
|
35237
|
+
return Response.json({ error: "grant_config_invalid" }, { status: 500 });
|
|
35238
|
+
}
|
|
35239
|
+
let target;
|
|
35240
|
+
try {
|
|
35241
|
+
target = new URL(req.url);
|
|
35242
|
+
} catch {
|
|
35243
|
+
return Response.json({ error: "grant_target_invalid" }, { status: 400 });
|
|
35244
|
+
}
|
|
35245
|
+
if (!originAllowed(config.origins, target)) {
|
|
35246
|
+
return Response.json({ error: "grant_origin_denied" }, { status: 403 });
|
|
35247
|
+
}
|
|
35248
|
+
if (!callerAllowed(config.allow, req.caller)) {
|
|
35249
|
+
return Response.json({ error: "grant_caller_denied" }, { status: 403 });
|
|
35250
|
+
}
|
|
35251
|
+
const headers = new Headers(req.headers);
|
|
35252
|
+
if (!injectGrantCredential(config, row.secret, target, headers)) {
|
|
35253
|
+
return Response.json({ error: "grant_kind_unsupported" }, { status: 501 });
|
|
35254
|
+
}
|
|
35255
|
+
try {
|
|
35256
|
+
const response = await fetch(target.toString(), {
|
|
35257
|
+
method: req.method,
|
|
35258
|
+
headers,
|
|
35259
|
+
body: req.body ?? void 0
|
|
35260
|
+
});
|
|
35261
|
+
return new Response(response.body, { status: response.status, headers: response.headers });
|
|
35262
|
+
} catch {
|
|
35263
|
+
return Response.json({ error: "grant_fetch_failed" }, { status: 502 });
|
|
35264
|
+
}
|
|
35265
|
+
}
|
|
35266
|
+
async handleGrantFetch(request, userId) {
|
|
35267
|
+
const body = await request.json().catch(() => null);
|
|
35268
|
+
if (!body?.grant || !body.url) {
|
|
35269
|
+
return Response.json({ error: "grant and url required" }, { status: 400 });
|
|
35270
|
+
}
|
|
35271
|
+
return this.grantFetch(userId, body.grant, {
|
|
35272
|
+
url: body.url,
|
|
35273
|
+
method: body.method ?? "GET",
|
|
35274
|
+
headers: body.headers ?? {},
|
|
35275
|
+
body: body.body ?? null,
|
|
35276
|
+
caller: body.caller ?? { kind: "unknown", name: "unknown" }
|
|
35277
|
+
});
|
|
35278
|
+
}
|
|
35279
|
+
async webSocketMessage(ws, _message) {
|
|
35280
|
+
if (!this.clientSockets.has(ws)) this.clientSockets.add(ws);
|
|
35281
|
+
}
|
|
34619
35282
|
async webSocketError(ws) {
|
|
34620
|
-
this.
|
|
34621
|
-
this.clientInfo.delete(ws);
|
|
35283
|
+
await this.webSocketClose(ws);
|
|
34622
35284
|
}
|
|
34623
35285
|
/** Whether a persistent client (daemon) is connected right now. */
|
|
34624
35286
|
async hasClient() {
|
|
@@ -34652,6 +35314,17 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34652
35314
|
this.clientInfo.delete(ws);
|
|
34653
35315
|
}
|
|
34654
35316
|
}
|
|
35317
|
+
const wakeEvent = JSON.stringify({
|
|
35318
|
+
event: "standardagents.wake",
|
|
35319
|
+
data: { thread_id: threadId }
|
|
35320
|
+
});
|
|
35321
|
+
for (const ws of this.streamSockets()) {
|
|
35322
|
+
try {
|
|
35323
|
+
ws.send(wakeEvent);
|
|
35324
|
+
delivered = true;
|
|
35325
|
+
} catch {
|
|
35326
|
+
}
|
|
35327
|
+
}
|
|
34655
35328
|
return { delivered };
|
|
34656
35329
|
}
|
|
34657
35330
|
// ─── Account-wide KV (sharded off the singleton) ───────────────────────────
|
|
@@ -34678,6 +35351,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34678
35351
|
userId,
|
|
34679
35352
|
key
|
|
34680
35353
|
);
|
|
35354
|
+
this.dispatchUserHooks(userId, "user_value_changed", [key]);
|
|
34681
35355
|
return;
|
|
34682
35356
|
}
|
|
34683
35357
|
const serialized = JSON.stringify(value);
|
|
@@ -34693,6 +35367,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
34693
35367
|
serialized,
|
|
34694
35368
|
now
|
|
34695
35369
|
);
|
|
35370
|
+
this.dispatchUserHooks(userId, "user_value_changed", [key]);
|
|
34696
35371
|
}
|
|
34697
35372
|
async listUserValues(userId, options = {}) {
|
|
34698
35373
|
this.ensureMigrated();
|