@sema-agent/server 7.66.0 → 7.68.0
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/USAGE.md +6 -3
- package/dist/approval-card.d.ts +8 -0
- package/dist/approval.d.ts +3 -0
- package/dist/approval.js +2 -1
- package/dist/boot/resolve-spec.js +13 -6
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-types.d.ts +15 -2
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/route-ctx.d.ts +5 -1
- package/dist/http/routes/diagnostics.d.ts +27 -2
- package/dist/http/routes/diagnostics.js +21 -1
- package/dist/http/routes/rules.js +15 -10
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +4 -8
- package/dist/http/server.d.ts +6 -1
- package/dist/http/server.js +7 -21
- package/dist/mcp-tool-faces.js +11 -2
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/permission-rule-vocab.d.ts +13 -21
- package/dist/permission-rule-vocab.js +6 -5
- package/dist/plugins/permission-rule-store-file.d.ts +3 -5
- package/dist/plugins/permission-rule-store-file.js +2 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
- package/dist/plugins/permission-rule-store-sql.js +35 -20
- package/dist/posture-source.d.ts +57 -0
- package/dist/posture-source.js +27 -0
- package/dist/read-face-posture.d.ts +9 -4
- package/dist/read-face-posture.js +7 -11
- package/dist/rules-consent.d.ts +15 -10
- package/dist/rules-consent.js +41 -21
- package/dist/runs.d.ts +10 -1
- package/dist/runs.js +2 -1
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +50 -6
- package/dist/task-mcp.js +71 -35
- package/dist/task-settings.d.ts +30 -0
- package/dist/task-settings.js +39 -18
- package/dist/tool-approval.d.ts +5 -4
- package/dist/tool-approval.js +2 -2
- package/dist/trace/engine-notice-wire.d.ts +17 -1
- package/dist/trace/engine-notice-wire.js +6 -1
- package/dist/trace/project.d.ts +4 -3
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPostureKnobReading } from "../../posture-source.js";
|
|
1
2
|
import { buildReadFaceWiring } from "../../read-face-posture.js";
|
|
2
3
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
3
4
|
import { sendJson, sendError } from "../send.js";
|
|
@@ -11,7 +12,21 @@ export function buildServerWiringGates(input) {
|
|
|
11
12
|
});
|
|
12
13
|
return {
|
|
13
14
|
streamApproval: gate.active ? "active" : gate.reason,
|
|
14
|
-
durableApproval: input.durableApproval,
|
|
15
|
+
durableApproval: buildPostureKnobReading(input.durableApproval, {
|
|
16
|
+
knob: "DURABLE_APPROVAL",
|
|
17
|
+
source: input.durableApprovalSource,
|
|
18
|
+
postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set) composed with a durable store being present: park/resume and the /v1/approvals routes are on by default — set DURABLE_APPROVAL=false to pin them off",
|
|
19
|
+
}),
|
|
20
|
+
streamAskWindowMs: buildPostureKnobReading(input.streamAskWindowMs, {
|
|
21
|
+
knob: "STREAM_ASK_WINDOW_MS",
|
|
22
|
+
source: input.streamAskWindowMsSource,
|
|
23
|
+
postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): an approval card waits 24h instead of 5 minutes, so stepping away does not settle the run unattended — set STREAM_ASK_WINDOW_MS to pin your own window",
|
|
24
|
+
}),
|
|
25
|
+
sessionAutoTitle: buildPostureKnobReading(input.sessionAutoTitle, {
|
|
26
|
+
knob: "SESSION_AUTO_TITLE",
|
|
27
|
+
source: input.sessionAutoTitleSource,
|
|
28
|
+
postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
|
|
29
|
+
}),
|
|
15
30
|
checkpointStore: input.checkpointStore !== undefined,
|
|
16
31
|
};
|
|
17
32
|
}
|
|
@@ -100,6 +115,11 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
|
|
|
100
115
|
backend: deps.backend,
|
|
101
116
|
checkpointStore: deps.checkpointStore,
|
|
102
117
|
durableApproval: deps.config.durableApproval === true,
|
|
118
|
+
durableApprovalSource: deps.config.durableApprovalSource,
|
|
119
|
+
streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
120
|
+
streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
|
|
121
|
+
sessionAutoTitle: deps.config.sessionAutoTitle === true,
|
|
122
|
+
sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
|
|
103
123
|
}),
|
|
104
124
|
});
|
|
105
125
|
return;
|
|
@@ -5,8 +5,8 @@ import { createLogger } from "../../observability/logger.js";
|
|
|
5
5
|
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
6
6
|
import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
|
|
7
7
|
import { HttpError } from "../../security.js";
|
|
8
|
-
import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
9
|
-
import { PERSISTED_RULE_MATCHES,
|
|
8
|
+
import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
|
|
9
|
+
import { PERSISTED_RULE_MATCHES, isPersistedRuleTool } from "../../permission-rule-vocab.js";
|
|
10
10
|
const logger = createLogger();
|
|
11
11
|
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
12
12
|
export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
@@ -46,7 +46,8 @@ const LocalImportRowSchema = z
|
|
|
46
46
|
.object({
|
|
47
47
|
rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
|
|
48
48
|
scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
|
|
49
|
-
|
|
49
|
+
behavior: z.enum(RULE_BEHAVIORS),
|
|
50
|
+
tool: z.string().min(1).max(190).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
|
|
50
51
|
match: z.enum(PERSISTED_RULE_MATCHES),
|
|
51
52
|
command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
|
|
52
53
|
adds: z.array(LocalImportAddSchema).max(64),
|
|
@@ -58,16 +59,18 @@ const LocalImportPrepareBodySchema = z.object({ rows: z.array(LocalImportRowSche
|
|
|
58
59
|
const LocalImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
|
|
59
60
|
const RuleRevokeBodySchema = z
|
|
60
61
|
.object({
|
|
62
|
+
behavior: z.enum(RULE_BEHAVIORS),
|
|
61
63
|
rule: z.string().min(1).max(1024),
|
|
62
64
|
scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
|
|
63
65
|
principal: z.string().min(1).max(190).optional(),
|
|
64
66
|
})
|
|
65
67
|
.strict();
|
|
68
|
+
const RULES_CURSOR_FORMAT = 2;
|
|
66
69
|
const RulesCursorSchema = z
|
|
67
|
-
.object({ rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
|
|
70
|
+
.object({ v: z.literal(RULES_CURSOR_FORMAT), rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
|
|
68
71
|
.strict();
|
|
69
72
|
function encodeRulesCursor(c) {
|
|
70
|
-
return Buffer.from(JSON.stringify(c)).toString("base64url");
|
|
73
|
+
return Buffer.from(JSON.stringify({ v: RULES_CURSOR_FORMAT, ...c })).toString("base64url");
|
|
71
74
|
}
|
|
72
75
|
function decodeRulesCursor(raw) {
|
|
73
76
|
try {
|
|
@@ -78,7 +81,7 @@ function decodeRulesCursor(raw) {
|
|
|
78
81
|
return { ok: false, why: err instanceof Error ? err.message : String(err) };
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
|
-
const rowKey = (r) => `${r.scope}\u0000${r.rule}`;
|
|
84
|
+
const rowKey = (r) => `${r.scope}\u0000${r.rule}\u0000${r.behavior}`;
|
|
82
85
|
export async function handleRules(req, res, url, ctx) {
|
|
83
86
|
const miss = { fell: false };
|
|
84
87
|
await handleRulesBody(req, res, url, ctx, miss);
|
|
@@ -132,7 +135,7 @@ async function handleRulesBody(req, res, url, ctx, miss) {
|
|
|
132
135
|
if (isLocalPrepare) {
|
|
133
136
|
const parsed = LocalImportPrepareBodySchema.safeParse(rawBody);
|
|
134
137
|
if (!parsed.success) {
|
|
135
|
-
sendError(res, 400, "request.body_shape", "body must be {rows:[{rule,scope,tool,match,command,adds}]}");
|
|
138
|
+
sendError(res, 400, "request.body_shape", "body must be {rows:[{behavior,rule,scope,tool,match,command,adds}]}");
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
138
141
|
const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
|
|
@@ -241,7 +244,9 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
|
|
|
241
244
|
sendError(res, 400, "request.query_invalid", "the rules cursor was minted at a different revision — restart the listing", { rev: listing.rev });
|
|
242
245
|
return;
|
|
243
246
|
}
|
|
244
|
-
const matching = listing.rows
|
|
247
|
+
const matching = listing.rows
|
|
248
|
+
.filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after))
|
|
249
|
+
.sort((x, y) => (rowKey(x) < rowKey(y) ? -1 : rowKey(x) > rowKey(y) ? 1 : 0));
|
|
245
250
|
const page = matching.slice(0, limit);
|
|
246
251
|
const last = page[page.length - 1];
|
|
247
252
|
const nextCursor = matching.length > limit && last !== undefined
|
|
@@ -252,7 +257,7 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
|
|
|
252
257
|
async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
|
|
253
258
|
const parsed = RuleRevokeBodySchema.safeParse(rawBody);
|
|
254
259
|
if (!parsed.success) {
|
|
255
|
-
sendError(res, 400, "request.body_shape", "body must be {rule, scope} with an optional principal");
|
|
260
|
+
sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
|
|
256
261
|
return;
|
|
257
262
|
}
|
|
258
263
|
const scope = parseRuleScope(parsed.data.scope);
|
|
@@ -265,7 +270,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
|
|
|
265
270
|
sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
|
|
266
271
|
return;
|
|
267
272
|
}
|
|
268
|
-
const outcome = await lane.removeRule({ principal: target.principal, rule: parsed.data.rule, scope });
|
|
273
|
+
const outcome = await lane.removeRule({ principal: target.principal, behavior: parsed.data.behavior, rule: parsed.data.rule, scope });
|
|
269
274
|
switch (outcome.status) {
|
|
270
275
|
case "removed":
|
|
271
276
|
sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1609,7 +1609,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
|
|
|
1609
1609
|
windowMs: deps.config.streamApproval?.windowMs,
|
|
1610
1610
|
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
1611
1611
|
}
|
|
1612
|
-
: undefined, deps.checkpointStore);
|
|
1612
|
+
: undefined, deps.checkpointStore, prepared.engineNotices);
|
|
1613
1613
|
return { status: 202, body: { taskId, sessionId, status: "running" } };
|
|
1614
1614
|
}, (r) => r.status === 202);
|
|
1615
1615
|
return resp;
|
|
@@ -231,20 +231,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
231
231
|
markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
|
|
232
232
|
let ledgerSink;
|
|
233
233
|
let ledgerTerminal = false;
|
|
234
|
-
if (detachOnDisconnect && durableTaskId && deps.runStore) {
|
|
235
|
-
const rs = deps.runStore;
|
|
236
|
-
const tid = durableTaskId;
|
|
237
|
-
ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
|
|
238
|
-
usageKey = durableTaskId;
|
|
239
|
-
if (usageKey)
|
|
240
|
-
deps.modelUsage?.register(usageKey);
|
|
241
|
-
}
|
|
242
234
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
243
235
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
244
236
|
}
|
|
245
237
|
if (durableTaskId && deps.runStore) {
|
|
246
238
|
const rs = deps.runStore;
|
|
247
239
|
const tid = durableTaskId;
|
|
240
|
+
ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
|
|
241
|
+
usageKey = tid;
|
|
242
|
+
deps.modelUsage?.register(tid);
|
|
248
243
|
const owner = principal ?? null;
|
|
249
244
|
const abortFromVerb = () => {
|
|
250
245
|
cancelledViaVerb.add(tid);
|
|
@@ -304,6 +299,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
304
299
|
const noticeLedger = ledgerSink;
|
|
305
300
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
306
301
|
sessionId: prepared.spec.sessionId,
|
|
302
|
+
...(prepared.engineNotices !== undefined ? { pending: prepared.engineNotices } : {}),
|
|
307
303
|
live: (row) => {
|
|
308
304
|
if (res.writableEnded || res.destroyed) {
|
|
309
305
|
recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
|
package/dist/http/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain } from "@sema-agent/core";
|
|
3
|
+
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
6
|
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
@@ -92,9 +92,14 @@ export interface ServiceCoreDeps {
|
|
|
92
92
|
* `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture ——
|
|
93
93
|
* bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。
|
|
94
94
|
* ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */
|
|
95
|
+
/** `opts.onNotice` (L-167, core 7.9.1):装配期铸下的、**这条会话**的用户面通告(今天只有一族:请求腿
|
|
96
|
+
* MCP 注入被丢弃)。装配跑在通告腿开口**之前**(`prepareSpec` → 路由注册 `registerEngineNoticeLeg`),
|
|
97
|
+
* 所以它不能在铸点直接 `route()` ——收下来交给调用腿,由腿在开口那一拍连同 `pending` 一起投。
|
|
98
|
+
* 缺席(桩、resume 家族)⇒ 只走结构化日志终点,行为逐字不变。 */
|
|
95
99
|
resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
|
|
96
100
|
leg?: "fresh" | "resume";
|
|
97
101
|
liveStream?: boolean;
|
|
102
|
+
onNotice?: (notice: EngineNotice) => void;
|
|
98
103
|
}) => TaskSpec | Promise<TaskSpec>;
|
|
99
104
|
/**
|
|
100
105
|
* [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
|
package/dist/http/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
|
|
3
3
|
import { once } from "node:events";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs,
|
|
5
|
+
import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
|
|
6
6
|
import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
|
|
7
7
|
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
8
8
|
import {} from "../config-center/facade.js";
|
|
@@ -24,7 +24,6 @@ import { publicStoreProbeError } from "../store-live-probe.js";
|
|
|
24
24
|
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
25
25
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
26
26
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
27
|
-
import { HANDS_BAND_TOOL_NAMES } from "../capabilities/hands-lane.js";
|
|
28
27
|
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
|
|
29
28
|
import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
|
|
30
29
|
import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
|
|
@@ -115,10 +114,10 @@ export function validateUserSkills(skills) {
|
|
|
115
114
|
}
|
|
116
115
|
return null;
|
|
117
116
|
}
|
|
117
|
+
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
118
118
|
const MAX_BODY = 8 * 1024 * 1024;
|
|
119
119
|
const MAX_IMAGES_PER_REQUEST = 20;
|
|
120
120
|
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
121
|
-
const HAND_TOOL_NAMES = HANDS_BAND_TOOL_NAMES;
|
|
122
121
|
function mentionableNames(config, catalog) {
|
|
123
122
|
const allowlist = config.atModelAllowlist ?? [];
|
|
124
123
|
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
|
|
@@ -323,6 +322,7 @@ export function createHttpServer(rawDeps) {
|
|
|
323
322
|
...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
|
|
324
323
|
configHash,
|
|
325
324
|
pid: process.pid,
|
|
325
|
+
startedAt: PROCESS_STARTED_AT_MS,
|
|
326
326
|
port: deps.config.port,
|
|
327
327
|
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
328
328
|
...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
|
|
@@ -997,8 +997,9 @@ export function createHttpServer(rawDeps) {
|
|
|
997
997
|
}
|
|
998
998
|
try {
|
|
999
999
|
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1000
|
+
const engineNotices = [];
|
|
1001
|
+
const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
|
|
1002
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body, ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
1002
1003
|
}
|
|
1003
1004
|
catch (err) {
|
|
1004
1005
|
if (err instanceof HttpError) {
|
|
@@ -1222,22 +1223,6 @@ export function createHttpServer(rawDeps) {
|
|
|
1222
1223
|
throw e;
|
|
1223
1224
|
}
|
|
1224
1225
|
const pendingTool = cp.pendingAction.toolName;
|
|
1225
|
-
const pendingToolCanon = pendingTool;
|
|
1226
|
-
const availableTools = new Set((spec.tools ?? []).map((t) => t.name));
|
|
1227
|
-
if (deps.config.remoteExec)
|
|
1228
|
-
for (const name of HAND_TOOL_NAMES)
|
|
1229
|
-
availableTools.add(name);
|
|
1230
|
-
if (deps.checkpointStore)
|
|
1231
|
-
availableTools.add("AskUserQuestion");
|
|
1232
|
-
if (spec.selfOrchestration === true && deps.config.selfOrchestrationEnabled) {
|
|
1233
|
-
availableTools.add(RUN_WORKFLOW_TOOL_NAME);
|
|
1234
|
-
}
|
|
1235
|
-
if (decision === "approve" && pendingToolCanon && !availableTools.has(pendingToolCanon)) {
|
|
1236
|
-
return {
|
|
1237
|
-
status: 422,
|
|
1238
|
-
body: { error: `pending action no longer satisfiable: tool "${pendingToolCanon}" is not in the current task config (scenario changed since suspend)`, errorCode: "conflict.pending_action_unsatisfiable" },
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
1226
|
if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
|
|
1242
1227
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
|
|
1243
1228
|
}
|
|
@@ -2434,6 +2419,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
2434
2419
|
env_factory_missing: true,
|
|
2435
2420
|
governed_unwired: true,
|
|
2436
2421
|
origin_missing: true,
|
|
2422
|
+
ledger_event_unknown: true,
|
|
2437
2423
|
real_approval_damaged: false,
|
|
2438
2424
|
real_approval_forged: false,
|
|
2439
2425
|
read_face_damaged: false,
|
package/dist/mcp-tool-faces.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ACCESSES } from "@sema-agent/core";
|
|
1
|
+
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES } from "@sema-agent/core";
|
|
2
2
|
function isRecord(v) {
|
|
3
3
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
4
|
}
|
|
@@ -26,17 +26,26 @@ function readOneFace(raw) {
|
|
|
26
26
|
const pt = raw.pathTarget;
|
|
27
27
|
if (!isRecord(pt) || typeof pt.param !== "string" || !memberOf(TOOL_PATH_ACCESSES, pt.access))
|
|
28
28
|
return undefined;
|
|
29
|
-
if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible"]))
|
|
29
|
+
if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible", "base", "absent", "patternParam"]))
|
|
30
30
|
return undefined;
|
|
31
31
|
if (pt.aliases !== undefined && !isStringArray(pt.aliases))
|
|
32
32
|
return undefined;
|
|
33
33
|
if (pt.skillScopeEligible !== undefined && typeof pt.skillScopeEligible !== "boolean")
|
|
34
34
|
return undefined;
|
|
35
|
+
if (pt.base !== undefined && !memberOf(TOOL_PATH_BASES, pt.base))
|
|
36
|
+
return undefined;
|
|
37
|
+
if (pt.absent !== undefined && !memberOf(TOOL_PATH_ABSENCES, pt.absent))
|
|
38
|
+
return undefined;
|
|
39
|
+
if (pt.patternParam !== undefined && typeof pt.patternParam !== "string")
|
|
40
|
+
return undefined;
|
|
35
41
|
out.pathTarget = {
|
|
36
42
|
param: pt.param,
|
|
37
43
|
access: pt.access,
|
|
38
44
|
...(pt.aliases !== undefined ? { aliases: pt.aliases } : {}),
|
|
39
45
|
...(pt.skillScopeEligible === true ? { skillScopeEligible: true } : {}),
|
|
46
|
+
...(pt.base !== undefined ? { base: pt.base } : {}),
|
|
47
|
+
...(pt.absent !== undefined ? { absent: pt.absent } : {}),
|
|
48
|
+
...(pt.patternParam !== undefined ? { patternParam: pt.patternParam } : {}),
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
51
|
if (raw.renderHints !== undefined) {
|
|
@@ -69,6 +69,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
69
69
|
readonly cls: "F";
|
|
70
70
|
readonly note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。";
|
|
71
71
|
};
|
|
72
|
+
readonly "server.session-titler.gateway-unreachable": {
|
|
73
|
+
readonly cls: "F";
|
|
74
|
+
readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
|
|
75
|
+
};
|
|
72
76
|
readonly "server.prompts.artifact-cache-write-failed": {
|
|
73
77
|
readonly cls: "F";
|
|
74
78
|
readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
|
|
@@ -60,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
60
60
|
cls: "F",
|
|
61
61
|
note: "elicitation_complete 面包屑持久化失败。同上——答案已经在壳手里,丢的是收尾渲染。",
|
|
62
62
|
},
|
|
63
|
+
"server.session-titler.gateway-unreachable": {
|
|
64
|
+
cls: "F",
|
|
65
|
+
note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
|
|
66
|
+
},
|
|
63
67
|
"server.prompts.artifact-cache-write-failed": {
|
|
64
68
|
cls: "F",
|
|
65
69
|
note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
|
|
@@ -260,7 +260,7 @@ export function createMetrics() {
|
|
|
260
260
|
m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
|
|
261
261
|
m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
|
|
262
262
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
263
|
-
m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
|
|
263
|
+
m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
|
|
264
264
|
m.counter("permission_rule_events_total", "Persisted allow-rule seam events by event (persisted_rule_allowed = a rule authorized a call with nobody asked; rule_store_unreadable = the store failed to read so the call was adjudicated with ZERO rules)");
|
|
265
265
|
m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
|
|
266
266
|
m.counter("hook_llm_calls_total", "prompt/agent hook entries that completed a model call (hooks 3b), by type");
|
|
@@ -1,29 +1,21 @@
|
|
|
1
|
+
import type { RuleApprovalRecord } from "@sema-agent/core";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `plugins/permission-rule-store-sql.ts` 的 `rules_json` 读/写形),core 给 `PersistedRuleMatch` 加两员
|
|
7
|
-
* (wildcard / subpath)、给 `PersistedRuleTool` 加 Read 时,四份抄件**没有一份**会编译红 —— zod 的 enum
|
|
8
|
-
* 只是运行期的一个 Set,静默把一次上游语义扩张翻成一整类合法行的拒收([ref] 词表纪律说的正是这形:
|
|
9
|
-
* 未知词必须是**编译**错,不是运行期一个安静的 false)。
|
|
3
|
+
* `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
|
|
4
|
+
* (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
|
|
5
|
+
* 「转录一份名单」换成「转 core 的**谓词**」——名单在 core 的目录里,抄一份的那天目录加一个带
|
|
6
|
+
* `pathTarget` 的工具,本仓会静默拒收一整类合法行(正是本文件顶注说的那个病)。
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* core 没有的词 ⇒ 同样红(词表属主是 core,本仓只转录)。
|
|
14
|
-
*
|
|
15
|
-
* ⚠️ 转录**不是**判据:一个词在表里只说「这是 core 定义的一员」,不说「本仓每条腿都接受它」。逐腿的收窄
|
|
16
|
-
* (例如卡编辑臂对 `subpath` 的拒)写在那条腿自己的穷举 `switch` 里,并各带理由。
|
|
8
|
+
* 谓词方向仍是 fail-CLOSED:`ruleToolGrammarOf` 认不出的名字**不是**一条可持久化规则的工具位
|
|
9
|
+
* (既非命令文法也非路径文法 ⇒ 这条行永远匹配不上任何调用),两只店与票口一律拒。
|
|
17
10
|
*/
|
|
18
|
-
|
|
19
|
-
/** `PersistedRuleTool`([ref] §2.2:`"Read"` 只说一种文法 —— `Read(//abs-dir/**)` 目录只读授权)。 */
|
|
20
|
-
export declare const PERSISTED_RULE_TOOLS: readonly ["Bash", "Read"];
|
|
11
|
+
export declare function isPersistedRuleTool(name: string): boolean;
|
|
21
12
|
/**
|
|
22
|
-
* `PersistedRuleMatch`
|
|
13
|
+
* `PersistedRuleMatch` 五员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
|
|
23
14
|
* 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
|
|
24
|
-
* `subpath`(Read 目录形,永不容纳任何命令)
|
|
15
|
+
* `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
|
|
16
|
+
* —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)。
|
|
25
17
|
*/
|
|
26
|
-
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath"];
|
|
18
|
+
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path"];
|
|
27
19
|
/** [ref] §2.3 B3:batch offer 成员的判别位(`command` = 历史逐段 Bash 规则;`directoryRead` = cd 段铸的目录只读授权)。 */
|
|
28
20
|
export declare const RULE_OFFER_BATCH_MEMBER_KINDS: readonly ["command", "directoryRead"];
|
|
29
21
|
/** [ref] §3.5:一段「为什么仍未被覆盖」的闭三词集(`redirection` 永久逐次 / `no_rule_form` 无可覆盖形 / `cap_overflow` 去重+帽挤出)。 */
|
|
@@ -31,7 +23,7 @@ export declare const UNCOVERED_SEGMENT_REASONS: readonly ["redirection", "no_rul
|
|
|
31
23
|
/** [ref] §3.3-5:卡编辑面的 breadth 警告码(记录 `edited.warnings` 存的就是这两个字面)。 */
|
|
32
24
|
export declare const EDITED_RULE_BREADTH_WARNING_CODES: readonly ["broad_prefix", "compound_prefix"];
|
|
33
25
|
/**
|
|
34
|
-
* [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批
|
|
26
|
+
* [ref] §4.5 / [ref] §3.5 —— 审批记录的**形版本号**(core `RuleApprovalRecord.schema`,本批 3→4;[ref] 三态 behavior 进候选行)。
|
|
35
27
|
* 类型绑 core 的字面型:core 再升号 ⇒ 这里编译红,两只 durable 店(SQL 双方言 + File)的 stale 判别与写侧钉
|
|
36
28
|
* 同批随之;判别是**等式**(`!==`)不是下限 —— 旧号的行按 stale 信封交还(不迁移、不 widen、不丢行),
|
|
37
29
|
* pending 卡短命,重触发即取新卡(B7/B8 既诺)。
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { ruleToolGrammarOf } from "@sema-agent/core";
|
|
2
|
+
export function isPersistedRuleTool(name) {
|
|
3
|
+
return ruleToolGrammarOf(name) !== undefined;
|
|
4
|
+
}
|
|
5
|
+
export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path"];
|
|
5
6
|
const MATCHES_PIN = true;
|
|
6
7
|
void MATCHES_PIN;
|
|
7
8
|
export const RULE_OFFER_BATCH_MEMBER_KINDS = ["command", "directoryRead"];
|
|
@@ -13,5 +14,5 @@ void REASONS_PIN;
|
|
|
13
14
|
export const EDITED_RULE_BREADTH_WARNING_CODES = ["broad_prefix", "compound_prefix"];
|
|
14
15
|
const WARNING_CODES_PIN = true;
|
|
15
16
|
void WARNING_CODES_PIN;
|
|
16
|
-
export const RULE_APPROVAL_RECORD_SCHEMA =
|
|
17
|
+
export const RULE_APPROVAL_RECORD_SCHEMA = 4;
|
|
17
18
|
//# sourceMappingURL=permission-rule-vocab.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type
|
|
1
|
+
import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type StaleRuleApprovalRecord } from "@sema-agent/core";
|
|
2
2
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
|
|
3
3
|
import type { PermissionRuleStoreBundle } from "../rules-consent.js";
|
|
4
|
+
import { type RuleCandidate } from "@sema-agent/core";
|
|
4
5
|
/**
|
|
5
6
|
* `RuleApprovalRecordStore` 的 File 形。
|
|
6
7
|
*
|
|
@@ -62,10 +63,7 @@ export declare class FileRuleImportTicketStore {
|
|
|
62
63
|
principal: string;
|
|
63
64
|
approvalId: string;
|
|
64
65
|
purpose: RuleTicketPurpose;
|
|
65
|
-
candidates: ReadonlyArray<
|
|
66
|
-
rule: string;
|
|
67
|
-
scope: RuleScope;
|
|
68
|
-
}>;
|
|
66
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
69
67
|
ttlMs: number;
|
|
70
68
|
}): Promise<RuleImportTicket>;
|
|
71
69
|
/** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
|
|
@@ -6,12 +6,13 @@ import { FileDurableRulePartitionProvider, AppendLog, ensureDir, readJsonlRecord
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
9
|
+
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
9
10
|
const RuleScopeSchema = z.union([
|
|
10
11
|
z.object({ kind: z.literal("global") }).strict(),
|
|
11
12
|
z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
|
|
12
13
|
]);
|
|
13
14
|
const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().int().nonnegative().safe() }).strict();
|
|
14
|
-
const RuleCandidateSchema = z.object({ rule: z.string(), scope: RuleScopeSchema }).strict();
|
|
15
|
+
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: RuleScopeSchema }).strict();
|
|
15
16
|
const RuleOffer2Schema = z.union([
|
|
16
17
|
z.object({ kind: z.literal("single"), candidate: z.number().int().nonnegative().safe() }).strict(),
|
|
17
18
|
z
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
|
|
2
|
+
import { type PermissionRuleWriter, type DurableRulePartition, type DurableRulePartitionProvider, type RuleScope, type RuleSyncFrontier, type RuleCandidate, type RuleApprovalRecord, type StaleRuleApprovalRecord, type RuleApprovalRecordStore, type RuleOwner } from "@sema-agent/core";
|
|
3
3
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
4
4
|
import type { PgQueryFn } from "./pg-query.js";
|
|
5
5
|
import { type SqlDriver } from "./sql-driver.js";
|
|
@@ -274,11 +274,14 @@ export declare const SQL_DB_NOW_MS: {
|
|
|
274
274
|
readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
|
|
275
275
|
readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
276
276
|
};
|
|
277
|
-
/** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
277
|
+
/** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
|
|
278
|
+
*
|
|
279
|
+
* 🔴 入参型收到 core 的 `RuleCandidate`(core 7.9.0 [ref] 起含 `behavior`)—— 规则的身份是
|
|
280
|
+
* **(behavior, 文本, 作用域) 三元组**,而这道摘要正是「prepare 那一刻的候选集」与「redeem 那一刻的
|
|
281
|
+
* 候选集」之间的等式。此前的形参写成 `{rule, scope}` 二元组:序列化器吃的是整只对象所以运行期**恰好**
|
|
282
|
+
* 覆盖了 behavior,但那是巧合不是保证 —— 哪天有人从一个只有二元组的地方喂进来,一次把候选从 `deny`
|
|
283
|
+
* 改写成 `allow`(文本一个字节不动)在这道等式下就认不出来了。收到域型让机器替人守这一条。 */
|
|
284
|
+
export declare function buildRulePayloadHash(candidates: ReadonlyArray<RuleCandidate>): string;
|
|
282
285
|
/**
|
|
283
286
|
* 票面三态协议的 SQL 实现。
|
|
284
287
|
*
|
|
@@ -315,10 +318,7 @@ export declare class SqlRuleImportTicketStore {
|
|
|
315
318
|
principal: string;
|
|
316
319
|
approvalId: string;
|
|
317
320
|
purpose: RuleTicketPurpose;
|
|
318
|
-
candidates: ReadonlyArray<
|
|
319
|
-
rule: string;
|
|
320
|
-
scope: RuleScope;
|
|
321
|
-
}>;
|
|
321
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
322
322
|
ttlMs: number;
|
|
323
323
|
}): Promise<RuleImportTicket>;
|
|
324
324
|
/**
|