@sema-agent/server 7.79.0 → 7.79.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/file-history-disclosure.d.ts +53 -0
- package/dist/file-history-disclosure.js +89 -0
- package/dist/http/admission.d.ts +56 -0
- package/dist/http/admission.js +560 -0
- package/dist/http/approval-sweeps.d.ts +21 -0
- package/dist/http/approval-sweeps.js +138 -0
- package/dist/http/dispatch.d.ts +96 -0
- package/dist/http/dispatch.js +499 -0
- package/dist/http/request-guards.d.ts +51 -0
- package/dist/http/request-guards.js +205 -0
- package/dist/http/resume-legs.d.ts +80 -0
- package/dist/http/resume-legs.js +1230 -0
- package/dist/http/server.d.ts +19 -104
- package/dist/http/server.js +23 -2589
- package/dist/main.js +2 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/plugins/file-history-store-sql.js +12 -11
- package/package.json +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
2
|
+
import { HttpError, principalFrom } from "../security.js";
|
|
3
|
+
import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
4
|
+
import { sendError } from "./send.js";
|
|
5
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
|
|
6
|
+
const MAX_BODY = 8 * 1024 * 1024;
|
|
7
|
+
export function createRequestGuards(ctx) {
|
|
8
|
+
const { deps } = ctx;
|
|
9
|
+
function quotaExceeded(req, res) {
|
|
10
|
+
if (!deps.costQuota)
|
|
11
|
+
return false;
|
|
12
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
13
|
+
if (!principal)
|
|
14
|
+
return false;
|
|
15
|
+
const d = deps.costQuota.check(principal);
|
|
16
|
+
if (d.allowed)
|
|
17
|
+
return false;
|
|
18
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
19
|
+
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
20
|
+
sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function ownerCostQuotaDenied(principal) {
|
|
24
|
+
if (!deps.costQuota || !principal)
|
|
25
|
+
return undefined;
|
|
26
|
+
const d = deps.costQuota.check(principal);
|
|
27
|
+
if (d.allowed)
|
|
28
|
+
return undefined;
|
|
29
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
30
|
+
return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
|
|
31
|
+
}
|
|
32
|
+
async function ownerFleetLeaseDenied(owner) {
|
|
33
|
+
if (!deps.fleetLease || !owner)
|
|
34
|
+
return undefined;
|
|
35
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
36
|
+
if (adm.ok)
|
|
37
|
+
return undefined;
|
|
38
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
39
|
+
return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
|
|
40
|
+
}
|
|
41
|
+
async function ownerLeaseDenied(res, owner) {
|
|
42
|
+
if (!deps.fleetLease || !owner)
|
|
43
|
+
return false;
|
|
44
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
45
|
+
if (adm.ok)
|
|
46
|
+
return false;
|
|
47
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
48
|
+
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
49
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function ownerQuotaDenied(res, owner) {
|
|
53
|
+
const denial = ownerCostQuotaDenied(owner);
|
|
54
|
+
if (!denial)
|
|
55
|
+
return false;
|
|
56
|
+
const { errorCode, error, ...extras } = denial.body;
|
|
57
|
+
res.setHeader("retry-after", String(extras.retryAfterSec));
|
|
58
|
+
sendError(res, denial.status, errorCode, error, extras);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
async function leaseDenied(req, res) {
|
|
62
|
+
if (!deps.fleetLease)
|
|
63
|
+
return false;
|
|
64
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
65
|
+
if (!principal)
|
|
66
|
+
return false;
|
|
67
|
+
const adm = await deps.fleetLease.admit(principal);
|
|
68
|
+
if (adm.ok)
|
|
69
|
+
return false;
|
|
70
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
71
|
+
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
72
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
async function usageWindowDenialFor(key) {
|
|
76
|
+
const store = deps.usageWindowStore;
|
|
77
|
+
const windows = deps.config.usageWindows;
|
|
78
|
+
if (!store || !windows || windows.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
let retryAfterMs;
|
|
81
|
+
try {
|
|
82
|
+
retryAfterMs = usageRetryAfterMs(await store.read(key, windows, Date.now()));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
deps.logger?.warn?.("usage_window_admission_read_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (retryAfterMs === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
deps.metrics?.inc("usage_window_rejected_total");
|
|
91
|
+
deps.logger?.info?.("usage_window_exhausted", { hasPrincipal: key !== GLOBAL_USAGE_KEY, retryAfterMs });
|
|
92
|
+
const retryAfterSec = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
93
|
+
return { status: 429, body: { error: "usage window exhausted (deployment token governance)", errorCode: "usage.window_exhausted", retryAfterSec }, retryAfterSec };
|
|
94
|
+
}
|
|
95
|
+
async function usageWindowDenied(req, res) {
|
|
96
|
+
const denial = await usageWindowDenialFor(gatedPrincipal(req, deps.config) || GLOBAL_USAGE_KEY);
|
|
97
|
+
if (!denial)
|
|
98
|
+
return false;
|
|
99
|
+
res.setHeader("retry-after", String(denial.retryAfterSec));
|
|
100
|
+
sendError(res, denial.status, denial.body.errorCode, denial.body.error, { retryAfterSec: denial.retryAfterSec });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function rateLimited(req, res) {
|
|
104
|
+
if (!deps.rateLimiter)
|
|
105
|
+
return false;
|
|
106
|
+
const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? req.socket.remoteAddress ?? "anon";
|
|
107
|
+
const d = deps.rateLimiter.check(key);
|
|
108
|
+
if (d.allowed)
|
|
109
|
+
return false;
|
|
110
|
+
deps.metrics?.inc("rate_limited_total");
|
|
111
|
+
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
112
|
+
sendError(res, 429, "limit.rate_exceeded", "rate limit exceeded", { retryAfterSec: d.retryAfterSec });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function safeDecode(seg) {
|
|
116
|
+
try {
|
|
117
|
+
return decodeURIComponent(seg);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isFleetWide(req) {
|
|
124
|
+
const { authToken: at, traceToken: tt } = deps.config;
|
|
125
|
+
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
126
|
+
return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
|
|
127
|
+
}
|
|
128
|
+
function sessionOwnerScope(req) {
|
|
129
|
+
const fleetWide = isFleetWide(req);
|
|
130
|
+
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
131
|
+
return { fleetWide, gateOwner };
|
|
132
|
+
}
|
|
133
|
+
function sessionOwnerScopeForWrite(req) {
|
|
134
|
+
const { authToken: at, traceToken: tt } = deps.config;
|
|
135
|
+
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
136
|
+
const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
|
|
137
|
+
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
138
|
+
return { fleetWide, gateOwner };
|
|
139
|
+
}
|
|
140
|
+
function runOwnerOk(req, res, owner, notFoundForm = "run") {
|
|
141
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
142
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
143
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!runOwnerReadable(principal, owner, deps.config)) {
|
|
147
|
+
if (notFoundForm === "session")
|
|
148
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
149
|
+
else
|
|
150
|
+
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const sessionAssertAbsentWarned = new Set();
|
|
156
|
+
function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
|
|
157
|
+
if (!run.sessionId)
|
|
158
|
+
return true;
|
|
159
|
+
const asserted = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
160
|
+
if (asserted === null) {
|
|
161
|
+
if (!sessionAssertAbsentWarned.has(face)) {
|
|
162
|
+
sessionAssertAbsentWarned.add(face);
|
|
163
|
+
console.warn(JSON.stringify({ ts: new Date().toISOString(), level: "warn", msg: "run_face_session_assert_absent", face, note: "caller sent no ?session= on a session-bound run — allowed in the accept phase; the enforcement train will 404 this ([1498]⑥/[1499])" }));
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (asserted === run.sessionId)
|
|
168
|
+
return true;
|
|
169
|
+
if (explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals))
|
|
170
|
+
return true;
|
|
171
|
+
sendError(res, 404, "not_found.run", notFoundError);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
async function readJson(req) {
|
|
175
|
+
const chunks = [];
|
|
176
|
+
let size = 0;
|
|
177
|
+
for await (const c of req) {
|
|
178
|
+
size += c.length;
|
|
179
|
+
if (size > MAX_BODY)
|
|
180
|
+
throw new HttpError(413, "request body too large");
|
|
181
|
+
chunks.push(c);
|
|
182
|
+
}
|
|
183
|
+
if (chunks.length === 0)
|
|
184
|
+
return {};
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new HttpError(400, "invalid JSON body");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readRawBody(req, max) {
|
|
193
|
+
const chunks = [];
|
|
194
|
+
let size = 0;
|
|
195
|
+
for await (const c of req) {
|
|
196
|
+
size += c.length;
|
|
197
|
+
if (size > max)
|
|
198
|
+
throw new HttpError(413, "blob too large");
|
|
199
|
+
chunks.push(c);
|
|
200
|
+
}
|
|
201
|
+
return Buffer.concat(chunks);
|
|
202
|
+
}
|
|
203
|
+
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=request-guards.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 续跑腿家族 + 路由 ctx 的装配(S-147 切片:`http/server.ts` 2378–4804 与 1119–1128 逐字搬家,正文一字未改)。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 为什么 `routeCtxBase` 住在这个文件里而不是组合根:`startFreshHostTurn` 把**整只** ctx 交给
|
|
5
|
+
* `mintAndDriveDurableRun`,而 `ctx.legs` 装的正是本文件这几条腿 —— 两者是一个结。拆开只有两条路:
|
|
6
|
+
* ①晚绑 `let routeCtxBase`(编译期钉不住「谁忘了 bind」,静默 undefined);②改写调用点正文(证不了
|
|
7
|
+
* 字节不变)。同居是第三条,也是唯一一条既保住静态序又保住 move-only 的。序由 `const` 的 TDZ 亲自执法:
|
|
8
|
+
* 腿是提升的函数声明、ctx 是文件尾的 `const`,谁也不会在工厂执行期读到对方。
|
|
9
|
+
*
|
|
10
|
+
* ctx 字段 28 只(registry 7 + helpers 14 + deps/runnerFor/prepareSpec/finalizeTaskResult + 三只配额/租约/用量窗
|
|
11
|
+
* 判据)—— 这是**如实披露**的耦合读数(设计稿 §9:把它变小是波 3 的事,本刀不承诺)。型一律从既有型派生
|
|
12
|
+
* (`RunRegistry` / `RouteHelpers` / `RouteLegs` / `ReturnType<typeof createRequestGuards>`),不手抄签名 ⇒ 不会漂。
|
|
13
|
+
*/
|
|
14
|
+
import type { IncomingMessage } from "node:http";
|
|
15
|
+
import { type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
|
|
16
|
+
import type { DecideBinding } from "./wire-types.js";
|
|
17
|
+
import type { CheckpointStoreFull } from "../plugins/store-backend.js";
|
|
18
|
+
import { type HostDecision } from "../host-decision.js";
|
|
19
|
+
import type { RouteCtxBase, DriveResumeArgs } from "./route-ctx.js";
|
|
20
|
+
import { type TrustedRunInternals } from "./durable-run-leg.js";
|
|
21
|
+
import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
|
|
22
|
+
import type { createRequestGuards } from "./request-guards.js";
|
|
23
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
24
|
+
export declare function createResumeLegs(ctx: {
|
|
25
|
+
deps: FlatServiceDeps;
|
|
26
|
+
runnerFor: NonNullable<FlatServiceDeps["runnerFor"]>;
|
|
27
|
+
} & RunRegistry & RouteHelpers & Pick<RouteLegs, "prepareSpec" | "finalizeTaskResult"> & Pick<ReturnType<typeof createRequestGuards>, "ownerCostQuotaDenied" | "ownerFleetLeaseDenied" | "usageWindowDenialFor">): {
|
|
28
|
+
routeCtxBase: RouteCtxBase;
|
|
29
|
+
resumeCheckpoint: (sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (grant: {
|
|
30
|
+
overrideSessionId?: string;
|
|
31
|
+
validatedToolName: string | null;
|
|
32
|
+
}) => Promise<void>, acceptEarly?: boolean, decider?: {
|
|
33
|
+
principal?: string;
|
|
34
|
+
explicitOperator: boolean;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
status: number;
|
|
37
|
+
body: object;
|
|
38
|
+
}>;
|
|
39
|
+
driveResumeIntoRunLog: (args: DriveResumeArgs) => Promise<{
|
|
40
|
+
status: number;
|
|
41
|
+
body: object;
|
|
42
|
+
}>;
|
|
43
|
+
driveResumeLeg: (args: DriveResumeArgs, onAccepted?: (receipt: {
|
|
44
|
+
status: number;
|
|
45
|
+
body: object;
|
|
46
|
+
}) => void) => Promise<{
|
|
47
|
+
status: number;
|
|
48
|
+
body: object;
|
|
49
|
+
}>;
|
|
50
|
+
mayDiscloseRowError: (cs: CheckpointStoreFull, token: CheckpointToken, decider: {
|
|
51
|
+
principal?: string;
|
|
52
|
+
explicitOperator: boolean;
|
|
53
|
+
} | undefined) => Promise<boolean>;
|
|
54
|
+
resumePreempted: (sessionId: string, req: IncomingMessage | undefined, acceptEarly?: boolean, decider?: {
|
|
55
|
+
principal?: string;
|
|
56
|
+
explicitOperator: boolean;
|
|
57
|
+
}) => Promise<{
|
|
58
|
+
status: number;
|
|
59
|
+
body: object;
|
|
60
|
+
}>;
|
|
61
|
+
startFreshHostTurn: (sessionId: string, objective: string, principal: string | undefined, req: IncomingMessage, trustedInternals: TrustedRunInternals, runId: string) => Promise<{
|
|
62
|
+
status: number;
|
|
63
|
+
body: object;
|
|
64
|
+
}>;
|
|
65
|
+
resumeWake: (sessionId: string, message: string | undefined, caller: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, workflowParkedResume?: DriveResumeArgs["workflowParkedResume"], ownerLeaseAdmittedFor?: string) => Promise<{
|
|
66
|
+
status: number;
|
|
67
|
+
body: object;
|
|
68
|
+
}>;
|
|
69
|
+
resumePlanReview: (sessionId: string, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, log?: {
|
|
70
|
+
taskId?: string;
|
|
71
|
+
principalPresent?: boolean;
|
|
72
|
+
}, acceptEarly?: boolean, decider?: {
|
|
73
|
+
principal?: string;
|
|
74
|
+
explicitOperator: boolean;
|
|
75
|
+
}) => Promise<{
|
|
76
|
+
status: number;
|
|
77
|
+
body: object;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=resume-legs.d.ts.map
|