@sema-agent/server 7.118.0 → 7.119.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/MIGRATION.md +85 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +24 -13
- package/deploy/sema-up/README.md +2 -0
- package/deploy/sema-up/chart/templates/minio.yaml +2 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
- package/deploy/sema-up/chart/templates/server.yaml +1 -0
- package/deploy/sema-up/chart/values.yaml +7 -1
- package/deploy/sema-up/cluster-up.sh +20 -8
- package/deploy/sema-up/kube-up.sh +29 -16
- package/deploy/sema-up/lib/buckets.sh +21 -0
- package/deploy/sema-up/lib/health.sh +33 -0
- package/deploy/sema-up/sema-up.sh +14 -9
- package/deploy/sema-up/smoke.sh +16 -6
- package/deploy/sema-up/stack/compose.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
- package/dist/approval-ask-audit-store.js +2 -7
- package/dist/approval-card.d.ts +5 -5
- package/dist/approval.d.ts +51 -5
- package/dist/approval.js +76 -13
- package/dist/audit.d.ts +12 -1
- package/dist/audit.js +62 -2
- package/dist/auto-mode-face.d.ts +2 -3
- package/dist/bake-runner/main.js +1 -6
- package/dist/bake-runner/protocol.d.ts +18 -0
- package/dist/bake-runner/protocol.js +22 -2
- package/dist/bake-runner/runner.d.ts +12 -8
- package/dist/bake-runner/runner.js +5 -9
- package/dist/boot/config-center.js +1 -1
- package/dist/boot/coordinators.js +4 -1
- package/dist/boot/device-lane.d.ts +3 -3
- package/dist/boot/device-lane.js +1 -2
- package/dist/boot/leader.d.ts +4 -4
- package/dist/boot/leader.js +1 -2
- package/dist/boot/parked-revive-gate.d.ts +5 -1
- package/dist/boot/parked-revive-gate.js +8 -2
- package/dist/boot/resolve-spec.js +7 -4
- package/dist/boot/stage-07-capability-layer.js +5 -1
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.js +12 -7
- package/dist/capabilities/center-plugins.d.ts +2 -0
- package/dist/capabilities/center-plugins.js +24 -6
- package/dist/capabilities/code-review-council.js +8 -11
- package/dist/capabilities/scenarios.d.ts +34 -7
- package/dist/capabilities/scenarios.js +33 -6
- package/dist/config-catalog.js +3 -3
- package/dist/config.js +2 -2
- package/dist/deployment-governance.d.ts +5 -2
- package/dist/deployment-governance.js +4 -3
- package/dist/host-decision.d.ts +52 -27
- package/dist/host-decision.js +14 -7
- package/dist/http/admission.js +6 -6
- package/dist/http/resume-legs.js +2 -1
- package/dist/http/routes/approvals-assistant.d.ts +25 -3
- package/dist/http/routes/approvals-assistant.js +9 -5
- package/dist/http/routes/capabilities.js +4 -3
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/runs.js +11 -5
- package/dist/http/routes/sessions.js +20 -16
- package/dist/http/wire-types.d.ts +12 -13
- package/dist/images/manifest.d.ts +7 -1
- package/dist/images/manifest.js +18 -16
- package/dist/memory-store-refusal.d.ts +7 -12
- package/dist/memory-store-refusal.js +4 -5
- package/dist/namespaced-rule-name.d.ts +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/operator-ask.d.ts +6 -5
- package/dist/plugins/approval-exemption-store.d.ts +4 -0
- package/dist/plugins/approval-exemption-store.js +1 -0
- package/dist/plugins/blob-backend.d.ts +13 -0
- package/dist/plugins/blob-backend.js +26 -0
- package/dist/plugins/local-session-store.d.ts +78 -47
- package/dist/plugins/local-session-store.js +152 -122
- package/dist/plugins/one-time-migrations.d.ts +6 -0
- package/dist/plugins/one-time-migrations.js +19 -1
- package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
- package/dist/plugins/snapshot-bucket-presence.js +40 -0
- package/dist/plugins/store-backend.d.ts +17 -6
- package/dist/plugins/store-backend.js +46 -16
- package/dist/plugins/store-open-wait.d.ts +33 -0
- package/dist/plugins/store-open-wait.js +73 -0
- package/dist/rule-entry-list.js +1 -1
- package/dist/run-local.js +4 -3
- package/dist/security.d.ts +5 -3
- package/dist/security.js +4 -0
- package/dist/session-policy-wire.js +7 -12
- package/dist/spec-fields.d.ts +6 -2
- package/dist/store-posture.d.ts +14 -6
- package/dist/store-posture.js +7 -4
- package/dist/task-settings.d.ts +27 -55
- package/dist/task-settings.js +6 -18
- package/dist/tool-approval.d.ts +97 -43
- package/dist/tool-approval.js +120 -31
- package/dist/trace/redact.d.ts +6 -0
- package/dist/trace/redact.js +9 -9
- package/dist/trace/self-minted-wire.d.ts +1 -1
- package/dist/user-input-ledger.d.ts +19 -15
- package/dist/user-input-ledger.js +12 -20
- package/package.json +2 -2
|
@@ -34,6 +34,8 @@ unset _self _hops _dir
|
|
|
34
34
|
. "$HERE/lib/shell-access.sh"
|
|
35
35
|
# 取值旗的形(S-621):判据与词表(--profile / --users / --region / --git / --kube)的唯一定义;--help 的词表也从这里渲。
|
|
36
36
|
. "$HERE/lib/flagshape.sh"
|
|
37
|
+
# 对象存储桶的唯一表(kube-up.sh / cluster-up.sh / smoke.sh 同读这一张)。
|
|
38
|
+
. "$HERE/lib/buckets.sh"
|
|
37
39
|
# 日志一律打到 stderr(stdout 留给 --interactive=json 的协议行)。定义在最前面:参数一解析完就要判形,那时下面的函数都还没定义。
|
|
38
40
|
log() { printf '[sema-up] %s\n' "$*" >&2; }
|
|
39
41
|
# 执行车道表的唯一读者(S-599)。定义在**最前面**:`--help` 的词表也从表渲(手抄的词表会在加一条
|
|
@@ -647,7 +649,7 @@ if $DRY_RUN; then
|
|
|
647
649
|
[ "$GIT_MODE" = "provision" ] && echo " 2. 先起 Gitea → 引导管理员+access token$([ "$REGISTRY_ENABLED" = "true" ] && printf '+OAuth app(接 registry 登录)')"
|
|
648
650
|
[ -n "$PG_URL$S3_ENDPOINT" ] && echo " 2b. 外接端点连接检测(PG=真 select 1;S3=兼容协议 ls+建桶;证书不校验)"
|
|
649
651
|
echo " 3. docker compose up -d --wait(profiles=$COMPOSE_PROFILES)$([ -n "$DATA_ROOT" ] && printf ';数据卷 bind → %s' "$DATA_ROOT")"
|
|
650
|
-
case ",$COMPOSE_PROFILES," in *,s3,*) echo " 4. MinIO 建桶
|
|
652
|
+
case ",$COMPOSE_PROFILES," in *,s3,*) echo " 4. MinIO 建桶 ${SEMA_UP_ENGINE_BUCKETS// //}(幂等)" ;; esac
|
|
651
653
|
[ "$GIT_MODE" = "connect" ] && echo " +. 验证 $GIT_URL 连通(Gitea→GitHub→通用 git 探测链,证书不校验;Gitea=自动建 OAuth app,其余=打印待人工项)"
|
|
652
654
|
echo " 5. 装后即验(PG/S3$([ "$PROFILE" != "data-center" ] && printf '/引擎 health')$([ "$REGISTRY_ENABLED" = "true" ] && printf '/registry healthz')$([ "$GIT_MODE" = "provision" ] && printf '/gitea healthz'))+ 一屏 summary"
|
|
653
655
|
echo " 端口: $PF_PORTS(bind:数据面 $([ "$PROFILE" = "data-center" ] && echo 0.0.0.0 || echo 127.0.0.1);服务/registry/git 0.0.0.0)"
|
|
@@ -874,9 +876,9 @@ fi
|
|
|
874
876
|
if [ -n "$S3_ENDPOINT" ]; then
|
|
875
877
|
log "外接 S3 连接检测($S3_ENDPOINT)"
|
|
876
878
|
S3OK=$(docker run --rm --entrypoint sh -e MC_HOST_x="$(printf '%s' "$S3_ENDPOINT" | sed "s#://#://$S3_ACCESS_KEY:$S3_SECRET_KEY@#")" \
|
|
877
|
-
"${MC_IMAGE:-minio/mc:latest}" -c
|
|
879
|
+
"${MC_IMAGE:-minio/mc:latest}" -c "mc --insecure ls x/ >/dev/null 2>&1 && mc --insecure mb --ignore-existing $(buckets_prefixed x/ $SEMA_UP_ENGINE_BUCKETS) >/dev/null 2>&1 && echo S3_OK" 2>/dev/null | tail -1)
|
|
878
880
|
[ "$S3OK" = "S3_OK" ] || { log "⛔ 外接 S3 连不上或建桶失败。下一步:核对 endpoint/密钥;需要 S3 兼容协议(MinIO/OSS/COS/R2 等);证书问题已用 --insecure 忽略,若还不通多半是网络/密钥"; exit 1; }
|
|
879
|
-
log "外接 S3 就绪(
|
|
881
|
+
log "外接 S3 就绪(${SEMA_UP_ENGINE_BUCKETS// //} 已确保)"
|
|
880
882
|
fi
|
|
881
883
|
|
|
882
884
|
# ── git=provision:先起 Gitea 并引导(OAuth app 的 client id/secret 要在 registry 起来之前
|
|
@@ -1005,13 +1007,16 @@ fi
|
|
|
1005
1007
|
# D(S-618):up 之前先 `compose config` 渲染对账 —— 容器真正拿到的决策键与本次决策逐值对齐才 up。
|
|
1006
1008
|
reconcile_render
|
|
1007
1009
|
log "装配:docker compose profiles=$COMPOSE_PROFILES project=$PROJECT"
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
# 建桶(幂等,一次性动作归 Layer 0;只对本地 MinIO——外接 S3 已在连接检测时确保)
|
|
1010
|
+
# 建桶(幂等,一次性动作归 Layer 0;只对本地 MinIO——外接 S3 已在连接检测时确保)。🔴 排在**全栈 up 之前**:引擎起服会核快照桶在不在
|
|
1011
|
+
# (缺则拒启),而 `up --wait` 要等引擎健康 —— 桶建在 up 之后,新数据卷上就是引擎等桶、compose 等引擎的互等。所以先只起 MinIO(`--wait` 等它健康)、建桶,再起全栈。
|
|
1011
1012
|
envf_load MINIO_PASSWORD # A:回读生成的 MinIO 口令(建桶用),不 source .env
|
|
1012
1013
|
case ",$COMPOSE_PROFILES," in *,s3,*)
|
|
1013
|
-
|
|
1014
|
+
COMPOSE_PROFILES="$COMPOSE_PROFILES" compose up -d --wait minio \
|
|
1015
|
+
|| { log "⛔ MinIO 启动失败。下一步:docker compose -p $PROJECT logs minio(常见:镜像拉取失败→检查镜像引用/docker login)"; exit 1; }
|
|
1016
|
+
docker run --rm --network "${PROJECT}_default" -e MC_HOST_x="http://sema:${MINIO_PASSWORD}@minio:9000" "${MC_IMAGE:-minio/mc:latest}" mb --ignore-existing $(buckets_prefixed x/ $SEMA_UP_ENGINE_BUCKETS) >/dev/null || { log "⛔ MinIO 建桶失败。下一步:docker compose -p $PROJECT logs minio"; exit 1; } ;;
|
|
1014
1017
|
esac
|
|
1018
|
+
COMPOSE_PROFILES="$COMPOSE_PROFILES" compose up -d --wait --remove-orphans \
|
|
1019
|
+
|| { log "⛔ compose up 失败。下一步:docker compose -p $PROJECT logs(常见:镜像拉取失败→检查镜像引用/docker login;端口占用→--port 换口)"; exit 1; }
|
|
1015
1020
|
|
|
1016
1021
|
# ── 验证(装后即验,§3d smoke 的子集;全量随时可跑 smoke.sh --project=$PROJECT)──────────────
|
|
1017
1022
|
# A:回读验证 / 汇总要用的凭据与登记值(不 source .env);缺的键回读成空,由下游 :- 缺省处置。
|
|
@@ -1066,9 +1071,9 @@ else
|
|
|
1066
1071
|
esac
|
|
1067
1072
|
[ -n "$PG_URL" ] && echo " PG(外接) $EXT_PG_HOST:$EXT_PG_PORT/$EXT_PG_DB(连接检测已过;凭据在 .env)"
|
|
1068
1073
|
case ",$COMPOSE_PROFILES," in *,s3,*)
|
|
1069
|
-
echo " S3/MinIO http://$HOSTIP:$MINIO_PORT(console :$MINIO_CONSOLE_PORT,桶×$V_BK
|
|
1074
|
+
echo " S3/MinIO http://$HOSTIP:$MINIO_PORT(console :$MINIO_CONSOLE_PORT,桶×$V_BK:${SEMA_UP_ENGINE_BUCKETS// //})" ;;
|
|
1070
1075
|
esac
|
|
1071
|
-
[ -n "$S3_ENDPOINT" ] && echo " S3(外接) $S3_ENDPOINT(
|
|
1076
|
+
[ -n "$S3_ENDPOINT" ] && echo " S3(外接) $S3_ENDPOINT(连接检测+引擎桶已确保;密钥在 .env)"
|
|
1072
1077
|
case ",$COMPOSE_PROFILES," in *,agent,*)
|
|
1073
1078
|
echo " 引擎 http://$HOSTIP:$SERVER_PORT(/health: $V_HEALTH;API 凭据=SERVICE_AUTH_TOKEN)"
|
|
1074
1079
|
compose_shell_access_text "$SERVER_PORT" ;;
|
package/deploy/sema-up/smoke.sh
CHANGED
|
@@ -22,6 +22,10 @@ unset _self _hops _dir
|
|
|
22
22
|
. "$HERE/lib/envf.sh"
|
|
23
23
|
# 取值旗的形(S-621):判据同 sema-up.sh 一处定义。
|
|
24
24
|
. "$HERE/lib/flagshape.sh"
|
|
25
|
+
# 引擎 /health 体里「落在哪个后端」的读法与期望值(kube-up / cluster-up 的验收同一只)。
|
|
26
|
+
. "$HERE/lib/health.sh"
|
|
27
|
+
# 对象存储桶的唯一表(sema-up.sh / kube-up.sh / cluster-up.sh 的建桶清单同读这一张)。
|
|
28
|
+
. "$HERE/lib/buckets.sh"
|
|
25
29
|
log() { printf '[smoke] %s\n' "$*" >&2; }
|
|
26
30
|
|
|
27
31
|
PROJECT="" URL="" TOKEN=""
|
|
@@ -68,9 +72,10 @@ if [ -n "$PROJECT" ]; then
|
|
|
68
72
|
# 哨兵判定(cat 回读值核对后 echo),不依赖各 mc 子命令的输出行为
|
|
69
73
|
S3RT=$(docker run --rm --entrypoint sh --network "${PROJECT}_default" -e MC_HOST_x="http://sema:${MINIO_PASSWORD}@minio:9000" "${MC_IMAGE:-minio/mc:latest}" -c 'echo smoke > /tmp/s && mc cp /tmp/s x/blobs/_smoke >/dev/null && [ "$(mc cat x/blobs/_smoke)" = smoke ] && mc rm x/blobs/_smoke >/dev/null && echo S3_ROUNDTRIP_OK' 2>/dev/null | tail -1)
|
|
70
74
|
[ "$S3RT" = "S3_ROUNDTRIP_OK" ] && ok "S3 put/get/rm 往返" || fail "S3" "对象读写失败" "docker compose -p $PROJECT logs minio"
|
|
71
|
-
# ③b
|
|
72
|
-
BKTS=$(docker run --rm --entrypoint sh --network "${PROJECT}_default" -e MC_HOST_x="http://sema:${MINIO_PASSWORD}@minio:9000" "${MC_IMAGE:-minio/mc:latest}" -c
|
|
73
|
-
|
|
75
|
+
# ③b 引擎桶逐个点名(data-center 验收:ls 行数≠桶名对——错桶也会凑数;清单 = lib/buckets.sh 那一张表)
|
|
76
|
+
BKTS=$(docker run --rm --entrypoint sh --network "${PROJECT}_default" -e MC_HOST_x="http://sema:${MINIO_PASSWORD}@minio:9000" "${MC_IMAGE:-minio/mc:latest}" -c "for b in $SEMA_UP_ENGINE_BUCKETS; do mc ls \"x/\$b\" >/dev/null 2>&1 && printf \"%s \" \"\$b\"; done" 2>/dev/null)
|
|
77
|
+
BKTS_WANT="$(printf '%s ' $SEMA_UP_ENGINE_BUCKETS)"
|
|
78
|
+
[ "$BKTS" = "$BKTS_WANT" ] && ok "桶就绪(${SEMA_UP_ENGINE_BUCKETS// //})" || fail "桶" "缺桶:应有 ${SEMA_UP_ENGINE_BUCKETS// //},实有 ${BKTS:-无}" "重跑 sema-up.sh(建桶幂等)"
|
|
74
79
|
fi
|
|
75
80
|
# 外接数据面(--pg-url/--s3-endpoint):有 endpoint 就远程直验(证书不校验)
|
|
76
81
|
if [ -n "${AGENT_PG_HOST:-}" ] && ! $HAS_PG && [ "${DEPLOY_PROFILE:-}" != "local-plus" ]; then
|
|
@@ -100,9 +105,14 @@ except Exception: print("")' 2>/dev/null)
|
|
|
100
105
|
fi
|
|
101
106
|
|
|
102
107
|
if [ -n "$URL" ]; then
|
|
103
|
-
# ④ /health
|
|
104
|
-
H=$(curl -m5 -s "$URL/health"
|
|
105
|
-
case "$H" in *'"status":"ok"'*) ok "/health($URL)" ;; *) fail "/health" "无响应或非 ok:${H:-空}" "服务没起/端口不对/防火墙;docker logs 或 systemctl 看进程" ;; esac
|
|
108
|
+
# ④ /health(整体取回:后端位在体的中后段,截断读会丢它)
|
|
109
|
+
H=$(curl -m5 -s "$URL/health")
|
|
110
|
+
case "$H" in *'"status":"ok"'*) ok "/health($URL)" ;; *) fail "/health" "无响应或非 ok:$(printf '%s' "${H:-空}" | head -c 200)" "服务没起/端口不对/防火墙;docker logs 或 systemctl 看进程" ;; esac
|
|
111
|
+
# ④′ 后端位:/health 200 ≠ 数据落了库 —— 引擎的 sessionBackend 必须是期望的持久后端(durable(pg)),`memory` = 内存里跑、重启即丢
|
|
112
|
+
case "$H" in *'"status":"ok"'*)
|
|
113
|
+
if BK_WHY=$(health_backend_check "$H"); then ok "引擎后端(sessionBackend=$SEMA_UP_EXPECT_SESSION_BACKEND)"
|
|
114
|
+
else fail "引擎后端" "$BK_WHY" "库没连上时引擎不会再退内存起服(拒启);读到 memory 多半是旧镜像或手工起的引擎 —— 看 docker logs / kubectl logs 的 store_sql_open_wait 与拒启句"; fi ;;
|
|
115
|
+
esac
|
|
106
116
|
if [ -n "$TOKEN" ]; then
|
|
107
117
|
# ⑤ auth 门:无 token 必须 401(部署安全底线)
|
|
108
118
|
C=$(curl -m5 -s -o /dev/null -w '%{http_code}' -X POST "$URL/v1/tasks" -H "Content-Type: application/json" -d '{}')
|
|
@@ -77,7 +77,8 @@ spec:
|
|
|
77
77
|
spec:
|
|
78
78
|
containers:
|
|
79
79
|
- name: minio
|
|
80
|
-
image: minio/minio:
|
|
80
|
+
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
|
81
|
+
imagePullPolicy: IfNotPresent
|
|
81
82
|
args: ["server", "/data", "--console-address", ":9001"]
|
|
82
83
|
env:
|
|
83
84
|
- { name: MINIO_ROOT_USER, valueFrom: { secretKeyRef: { name: sema-minio, key: rootUser } } }
|
|
@@ -155,6 +156,8 @@ spec:
|
|
|
155
156
|
env:
|
|
156
157
|
- { name: PORT, value: "8090" }
|
|
157
158
|
- { name: DB_BACKEND, value: pg }
|
|
159
|
+
# 引擎 boot 期等库的窗(ms):CNPG 与 server Pod 同时起,库初始化常慢于 server 就绪 —— 窗内重试、到窗仍连不上才拒启(chart 的 server.storeWaitMs 同值)
|
|
160
|
+
- { name: STORE_LOCK_WAIT_MS, value: "300000" }
|
|
158
161
|
- { name: SESSION_BACKEND, value: auto }
|
|
159
162
|
- { name: PG_HOST, value: sema-pg-rw.sema.svc }
|
|
160
163
|
- { name: PG_PORT, value: "5432" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { AppendLog, ensureDir } from "@sema-agent/core";
|
|
3
|
+
import { AppendLog, ensureDir, isHostDecider } from "@sema-agent/core";
|
|
4
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
5
|
import { replayJsonlSkipping } from "./observability/corrupt-read-seat.js";
|
|
6
6
|
export const APPROVAL_ASK_AUDIT_DIR = "approval-ask-audit";
|
|
@@ -10,11 +10,6 @@ export const AUDIT_SHUTDOWN_CAUSE_WORDS = { shutdown_before_park: true };
|
|
|
10
10
|
export function buildCheckpointAnchorHash(token) {
|
|
11
11
|
return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 16);
|
|
12
12
|
}
|
|
13
|
-
const HOST_DECIDED_BY_WORDS = ["person", "sla_timeout", "policy"];
|
|
14
|
-
const HOST_DECIDED_BY = new Set(HOST_DECIDED_BY_WORDS);
|
|
15
|
-
function isHostDecidedBy(v) {
|
|
16
|
-
return typeof v === "string" && HOST_DECIDED_BY.has(v);
|
|
17
|
-
}
|
|
18
13
|
function isAuditShutdownCause(v) {
|
|
19
14
|
return typeof v === "string" && Object.hasOwn(AUDIT_SHUTDOWN_CAUSE_WORDS, v);
|
|
20
15
|
}
|
|
@@ -123,7 +118,7 @@ function readLedgerRecord(u, noteOutOfSetSeat) {
|
|
|
123
118
|
const decidedByV = o.decidedBy;
|
|
124
119
|
const legacySettledByV = decidedByV === undefined ? o.settledBy : undefined;
|
|
125
120
|
const legacySettledBy = legacySettledByV === "human" || legacySettledByV === "timeout" || legacySettledByV === "aborted" ? legacySettledByV : undefined;
|
|
126
|
-
const decidedBy =
|
|
121
|
+
const decidedBy = isHostDecider(decidedByV) ? decidedByV : legacySettledBy !== undefined ? "person" : undefined;
|
|
127
122
|
const approver = s("approver");
|
|
128
123
|
const settledAtMs = num("settledAtMs");
|
|
129
124
|
const sessionId = s("sessionId");
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -695,8 +695,8 @@ export declare const ApprovalCardSchema: z.ZodObject<{
|
|
|
695
695
|
}, z.core.$strict>>>;
|
|
696
696
|
personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
|
|
697
697
|
}, z.core.$strict>>;
|
|
698
|
-
askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
|
|
699
|
-
classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
|
|
698
|
+
askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
|
|
699
|
+
classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
|
|
700
700
|
fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
701
701
|
sourceTaskId: z.ZodOptional<z.ZodString>;
|
|
702
702
|
sourceAgentName: z.ZodOptional<z.ZodString>;
|
|
@@ -825,8 +825,8 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
|
|
|
825
825
|
}, z.core.$strict>>>;
|
|
826
826
|
personalRuleDotsAbsent: z.ZodOptional<z.ZodCustom<AskEvidenceAbsence, AskEvidenceAbsence>>;
|
|
827
827
|
}, z.core.$strict>>;
|
|
828
|
-
askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
|
|
829
|
-
classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
|
|
828
|
+
askClass: z.ZodOptional<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>;
|
|
829
|
+
classes: z.ZodOptional<z.ZodArray<z.ZodCustom<"mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary", "mode" | "hook" | "org_rule" | "write_protection" | "write_protection_unresolved" | "content_question" | "ask_rule" | "denial_limit_fallback" | "ancestor" | "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "governance_unavailable" | "rule_unreadable" | "classifier_no_ruling" | "requires_user_interaction" | "operator_forced" | "shell_floor" | "probe" | "tool_marks_mcp" | "tool_marks_a2a" | "policy_rra" | "artifact_shared_read" | "unstated" | "ordinary">>>;
|
|
830
830
|
fromSubagent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
831
831
|
sourceTaskId: z.ZodOptional<z.ZodString>;
|
|
832
832
|
sourceAgentName: z.ZodOptional<z.ZodString>;
|
|
@@ -1001,7 +1001,7 @@ export declare function buildRevokeFrame(batchId: string, askIds: readonly strin
|
|
|
1001
1001
|
export declare function buildReplayFrame(row: AskRow, nowMs: number): ApprovalRequestFrame | undefined;
|
|
1002
1002
|
/**
|
|
1003
1003
|
* S-1154 —— 持久行上那张卡**要不要人**:卡面 `risk.requiresRealApproval`(铸卡时取自引擎在 `AskRequest` 上盖的同一位,
|
|
1004
|
-
* {@link buildApprovalCard} 第三参)。读者 = 耐久 ask
|
|
1004
|
+
* {@link buildApprovalCard} 第三参)。读者 = 耐久 ask 回决口在裁决之前交给引擎判官 `policyCannotAnswer` 的那一问(`host-decision.ts` 的 `heldAskOf`)。
|
|
1005
1005
|
* 🔴 读不出卡(`card_json` 形不合)⇒ **按要人读**(fail-closed):这一位只用来**拒**策略的放行,读坏的行上拒一次可以改成人答 / 策略拒
|
|
1006
1006
|
* 再来;反方向(读坏当不要人)会让一次引擎终将拒掉的放行先落成一条宣称 approve 的行。
|
|
1007
1007
|
*/
|
package/dist/approval.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* (createOaApprovalPolicy + ApprovalStore pending 行轮询)整体退役;唯一门形 = durable checkpoint
|
|
4
4
|
* suspend/resume,见 createDurableAskPolicy 与 /v1/approvals 的 checkpointStore 腿)。
|
|
5
5
|
*/
|
|
6
|
-
import { type ToolPolicy } from "@sema-agent/core";
|
|
6
|
+
import { type ToolCallRequest, type ToolPolicy } from "@sema-agent/core";
|
|
7
7
|
import { type PostureSource } from "./posture-source.js";
|
|
8
8
|
import { type ExecutionLane } from "./execution-lane-caps.js";
|
|
9
|
+
import { type ApprovalExemptionStore } from "./plugins/approval-exemption-store.js";
|
|
9
10
|
/**
|
|
10
11
|
* 门意图判据读的五个量(`ServiceConfig` 结构满足)。单独成型是为了让**热路径**能拿「候选视图」来判
|
|
11
12
|
* (S-668 波 2:`{ ...config, approvalRequire: 候选名单 }`)—— 判据只有一份,输入可以是活配置也可以是候选。
|
|
@@ -132,11 +133,56 @@ export declare function buildForceDurableGateInertNotice(input: {
|
|
|
132
133
|
* replica. (The former poll-lane
|
|
133
134
|
* twin, createOaApprovalPolicy, was retired in 5.0.0 — durable is the only gate.)
|
|
134
135
|
*/
|
|
135
|
-
export interface DurableAskOptions {
|
|
136
|
-
requireApproval: string[];
|
|
137
|
-
deny?: string[];
|
|
136
|
+
export interface DurableAskOptions extends ApprovalListsView {
|
|
138
137
|
autoBudget?: number;
|
|
139
|
-
|
|
138
|
+
/** 本会话的「本会话放行」读面(server 的会话豁免店,键 = 会话 × wire 工具名)。缺席 = 这条腿没有会话 / 没有店 ⇒ 名单门照问。 */
|
|
139
|
+
exemption?: DeploymentListExemption;
|
|
140
140
|
}
|
|
141
|
+
/** 三张运维名单(一只调用由哪一张说话)的入参 —— 名单门与「问的属主」读法同读这一份。 */
|
|
142
|
+
export interface ApprovalListsView {
|
|
143
|
+
readonly requireApproval: readonly string[];
|
|
144
|
+
readonly deny?: readonly string[];
|
|
145
|
+
readonly neverAuto?: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 三张运维名单对**一只调用**的判词(deny 先于 never-auto 先于 require),`hit` = 命中的那个名(规则归因写运维名单里的那个)。
|
|
149
|
+
* 名单门按它出 deny / 恒问人 / 名单的问;「这张卡上有没有名单自己的问」也按它读(见 {@link deploymentListAsks})—— 一份读法,两个读者。
|
|
150
|
+
*/
|
|
151
|
+
export type ApprovalListVerdict = {
|
|
152
|
+
readonly list: "deny";
|
|
153
|
+
} | {
|
|
154
|
+
readonly list: "never_auto" | "require";
|
|
155
|
+
readonly hit: string;
|
|
156
|
+
} | {
|
|
157
|
+
readonly list: "none";
|
|
158
|
+
};
|
|
159
|
+
/** 铸一只判词函数(名单的命中集与 covering 规则只解一次)。名字读法 = {@link heldRuleName}(收紧臂读调用答得上的全部名字)。 */
|
|
160
|
+
export declare function createApprovalListVerdict(lists: ApprovalListsView): (call: Pick<ToolCallRequest, "toolName" | "face">) => ApprovalListVerdict;
|
|
161
|
+
/**
|
|
162
|
+
* **「问的属主」的读法**:部署名单门对这只调用出不出它**自己的**那一问(`APPROVAL_REQUIRE` 臂)—— 读的是铸点本身(同一只判词),
|
|
163
|
+
* 不另造一张表。读者:审批席(活卡的提示位 / 回决写侧)、耐久 `/decide` 腿与运维队列行(停驻行上只有工具名,按 wire 名读)。
|
|
164
|
+
* 名单是热换的(中心下发),读者每次现读活配置。答 `true` 只说「名单门问这只调用」;这张卡是不是名单的那一问,还要看卡上的类
|
|
165
|
+
* (`tool-approval.ts` 的 `StandingAnswerFacts.deploymentListQuestion`)。
|
|
166
|
+
*/
|
|
167
|
+
export declare function deploymentListAsks(config: Pick<GateIntentView, "approvalRequire" | "approvalDeny" | "approvalNeverAuto">, call: Pick<ToolCallRequest, "toolName" | "face">): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* 名单门的「本会话放行」读面:本会话的豁免店读一次 + server 自己的审计行。名单的问是 server 铸的,它的站着的答案记在 server 的
|
|
170
|
+
* 会话豁免店(活卡的 `allow_session` 与耐久 `/decide` 的 `remember:"session"` 写同一只店),名单门在出问**之前**读它。
|
|
171
|
+
*/
|
|
172
|
+
export interface DeploymentListExemption {
|
|
173
|
+
/** 这只工具(wire 名)在本会话有没有豁免行。有界、fail-closed:读抛 / 超时 ⇒ `false`(照问,人多看一张卡)。 */
|
|
174
|
+
readonly has: (toolName: string) => Promise<boolean>;
|
|
175
|
+
/** 名单门因为豁免行没出问 —— server 自己的审计行(宿主豁免;不是人答的、不是策略代答的、没有记住任何东西)。 */
|
|
176
|
+
readonly onExempted: (toolName: string) => void;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 名单门豁免读面的**唯一**铸点(HTTP 腿 `resolve-spec` 与 park 赎回腿 `parked-revive-gate` 两处装配都经它):键 = 会话 × wire 工具名
|
|
180
|
+
* (与活卡 `allow_session` / `/decide` remember 写下的键同形)。审计事件名 {@link APPROVAL_EXEMPTED_EVENT},字段只有会话、工具与读位
|
|
181
|
+
* `seat: "deployment_list"`(审批席自己那一行是 `seat: "approval_seat"`)。读的期限与审批席读同一只店的期限同值。
|
|
182
|
+
*/
|
|
183
|
+
export declare function createDeploymentListExemption(store: Pick<ApprovalExemptionStore, "has">, sessionId: string, logger: {
|
|
184
|
+
info(event: string, fields?: Record<string, unknown>): void;
|
|
185
|
+
warn(event: string, fields?: Record<string, unknown>): void;
|
|
186
|
+
}, leg?: "parked_revive"): DeploymentListExemption;
|
|
141
187
|
export declare function createDurableAskPolicy(opts: DurableAskOptions): ToolPolicy;
|
|
142
188
|
//# sourceMappingURL=approval.d.ts.map
|
package/dist/approval.js
CHANGED
|
@@ -4,6 +4,8 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
|
|
|
4
4
|
import { isExplicitPosture } from "./posture-source.js";
|
|
5
5
|
import { handsOnWorkerFilePlane } from "./execution-lane-caps.js";
|
|
6
6
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
7
|
+
import { DURABLE_CONVERGE_READ_TIMEOUT_MS, policyAskStandingAnswerFacts, standingAnswerDurablyRecordable } from "./tool-approval.js";
|
|
8
|
+
import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
|
|
7
9
|
export function hasApprovalListIntent(config) {
|
|
8
10
|
return (config.approvalRequire.length > 0 ||
|
|
9
11
|
config.approvalDeny.length > 0 ||
|
|
@@ -67,6 +69,49 @@ export function buildForceDurableGateInertNotice(input) {
|
|
|
67
69
|
},
|
|
68
70
|
};
|
|
69
71
|
}
|
|
72
|
+
export function createApprovalListVerdict(lists) {
|
|
73
|
+
const require = createNameSetMatcher(lists.requireApproval);
|
|
74
|
+
const deny = createNameSetMatcher(lists.deny ?? []);
|
|
75
|
+
const neverAuto = createNameSetMatcher(lists.neverAuto ?? []);
|
|
76
|
+
return (call) => {
|
|
77
|
+
if (heldRuleName(call, deny) !== undefined)
|
|
78
|
+
return { list: "deny" };
|
|
79
|
+
const neverAutoHit = heldRuleName(call, neverAuto);
|
|
80
|
+
if (neverAutoHit !== undefined)
|
|
81
|
+
return { list: "never_auto", hit: neverAutoHit };
|
|
82
|
+
const requireHit = heldRuleName(call, require);
|
|
83
|
+
if (requireHit !== undefined)
|
|
84
|
+
return { list: "require", hit: requireHit };
|
|
85
|
+
return { list: "none" };
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function deploymentListAsks(config, call) {
|
|
89
|
+
return createApprovalListVerdict({ requireApproval: config.approvalRequire, deny: config.approvalDeny, neverAuto: config.approvalNeverAuto })(call).list === "require";
|
|
90
|
+
}
|
|
91
|
+
export function createDeploymentListExemption(store, sessionId, logger, leg) {
|
|
92
|
+
const where = { sessionId, ...(leg !== undefined ? { leg } : {}) };
|
|
93
|
+
return {
|
|
94
|
+
has: async (toolName) => {
|
|
95
|
+
let timer;
|
|
96
|
+
try {
|
|
97
|
+
const deadline = new Promise((_, reject) => {
|
|
98
|
+
timer = setTimeout(() => reject(new Error(`exemption read timed out after ${DURABLE_CONVERGE_READ_TIMEOUT_MS}ms`)), DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
99
|
+
timer.unref?.();
|
|
100
|
+
});
|
|
101
|
+
return (await Promise.race([store.has(sessionId, toolName), deadline])) === true;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.warn("session_grant_read_failed", { ...where, toolName, seat: "deployment_list", error: String(err).slice(0, 200) });
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (timer !== undefined)
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
onExempted: (toolName) => logger.info(APPROVAL_EXEMPTED_EVENT, { ...where, toolName, seat: "deployment_list" }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
70
115
|
function createNameSetMatcher(names) {
|
|
71
116
|
const exact = new Set(names);
|
|
72
117
|
const covering = names.filter(isNamespacedCoveringRuleName);
|
|
@@ -78,30 +123,48 @@ function grantedByRule() {
|
|
|
78
123
|
return { action: "allow", decisionReason: "rule" };
|
|
79
124
|
}
|
|
80
125
|
export function createDurableAskPolicy(opts) {
|
|
81
|
-
const
|
|
82
|
-
const deny = createNameSetMatcher(opts.deny ?? []);
|
|
83
|
-
const neverAuto = createNameSetMatcher(opts.neverAuto ?? []);
|
|
126
|
+
const verdictOf = createApprovalListVerdict(opts);
|
|
84
127
|
const budget = Math.max(0, Math.floor(opts.autoBudget ?? 0));
|
|
85
128
|
let autoApproved = 0;
|
|
86
129
|
const isQuestionTool = (name) => name === ASK_USER_QUESTION_TOOL_NAME;
|
|
87
130
|
return {
|
|
88
131
|
check: (req) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
132
|
+
const verdict = verdictOf(req);
|
|
133
|
+
switch (verdict.list) {
|
|
134
|
+
case "deny":
|
|
135
|
+
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
136
|
+
case "never_auto":
|
|
137
|
+
return operatorAlwaysHumanAsk(`APPROVAL_NEVER_AUTO(${verdict.hit})`, `human approval required to run "${req.toolName}"`);
|
|
138
|
+
case "require":
|
|
139
|
+
return decideRequired(req, verdict.hit);
|
|
140
|
+
case "none":
|
|
141
|
+
return { action: "allow" };
|
|
142
|
+
default: {
|
|
143
|
+
const unreachable = verdict;
|
|
144
|
+
throw new Error(`unhandled approval-list verdict: ${JSON.stringify(unreachable)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
98
147
|
},
|
|
99
148
|
};
|
|
100
|
-
function decideRequired(req,
|
|
149
|
+
function decideRequired(req, requireHit) {
|
|
101
150
|
const rem = req.budget?.resourceRemainingMicroUsd;
|
|
102
151
|
if (rem !== undefined && rem <= 0) {
|
|
103
152
|
return operatorAlwaysHumanAsk("RESOURCE_SUSPEND budget exhausted", `durable resource budget exhausted — human approval required to run "${req.toolName}"`);
|
|
104
153
|
}
|
|
154
|
+
const ask = operatorAsk(`APPROVAL_REQUIRE(${requireHit})`, `approval required to run "${req.toolName}"`);
|
|
155
|
+
if (opts.exemption !== undefined && standingAnswerDurablyRecordable(policyAskStandingAnswerFacts(req, ask, true))) {
|
|
156
|
+
const exemption = opts.exemption;
|
|
157
|
+
return (async () => {
|
|
158
|
+
if (await exemption.has(req.toolName)) {
|
|
159
|
+
exemption.onExempted(req.toolName);
|
|
160
|
+
return { action: "allow" };
|
|
161
|
+
}
|
|
162
|
+
return budgetOrAsk(req, ask);
|
|
163
|
+
})();
|
|
164
|
+
}
|
|
165
|
+
return budgetOrAsk(req, ask);
|
|
166
|
+
}
|
|
167
|
+
function budgetOrAsk(req, ask) {
|
|
105
168
|
if (heldRuleName(req, isQuestionTool) === undefined && budget > 0 && autoApproved < budget) {
|
|
106
169
|
autoApproved++;
|
|
107
170
|
return grantedByRule();
|
package/dist/audit.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface SessionAudit {
|
|
|
16
16
|
provider: string;
|
|
17
17
|
modelId: string;
|
|
18
18
|
} | null;
|
|
19
|
-
/** Current model context: prior messages with older history collapsed into a summary message.
|
|
19
|
+
/** Current model context: prior messages with older history collapsed into a summary message.
|
|
20
|
+
* 🔴 **存档原形**(store 里怎么写就怎么回)—— 不是 wire 形。HTTP 读面一律过 {@link redactSessionViewForRead}(次序 = 选窗 → 脱敏 → 截断)
|
|
21
|
+
* 再回给调用方;直接消费本视图的宿主(`buildSessionAudit` 是包根导出)自己负责同一道脱敏。 */
|
|
20
22
|
messages: unknown[];
|
|
21
23
|
/** [ref]① web 观测切片 B: the session's prompt-epoch pin, core contract verbatim (core 1.304
|
|
22
24
|
* `session.getPromptEpoch()` — first-class `prompt_epoch` entry, compaction restatement, [ref] anchor
|
|
@@ -36,10 +38,19 @@ export declare function buildSessionAudit(pool: Pool, sessionId: string): Promis
|
|
|
36
38
|
* (its storage seam has no wake-equivalent yet — may turn out to be a core gap). */
|
|
37
39
|
export declare function buildSessionAuditPg(pool: PgPool, sessionId: string): Promise<SessionAudit | undefined>;
|
|
38
40
|
/** The LOCAL (File) leg — web session area on DB_BACKEND=local.
|
|
41
|
+
* repo 取自 `createLocalSessionRepo`(与 `LocalSessionStore` 同一只工厂):坏记录的披露座(F 类 `server.session-repo.corrupt-record-skipped`)
|
|
42
|
+
* 因此在视图口同样在场 —— 视图照常返回、跳过的记录有计数 + 探针行(修前自建一只不带座的 repo,零披露)。
|
|
39
43
|
* 实现姿势:`FileSessionRepo.exportEntries`(纯读、torn-tail 恢复、missing→not_found)→
|
|
40
44
|
* `boundedTail`(纯函数切 floor)→ InMemorySessionStorage → 同一 auditFromStorage 内核。零写面、
|
|
41
45
|
* 零新 seam;createdAt 从 repo.list() 的 metadata 取(File 形态 meta 与 entries 分储)。 */
|
|
42
46
|
export declare function buildSessionAuditLocal(root: string, sessionId: string): Promise<SessionAudit | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* 🔴 **会话视图的读面口 —— `GET /v1/sessions/:id` 的全部取法(整读 / `?tail` / `?before` / `?blobCap` / `?message=N`)共用这一只**。
|
|
49
|
+
* 次序 = **选 → 脱 → 截**(路由 `http/routes/sessions.ts`):整条消息的选窗不切字符串,先选窗不改任何一条的脱敏读数、只把脱敏的量降到选中的那几条;
|
|
50
|
+
* `blobCap` 的截断会切字符串(预览切在密钥中间,半截密钥认不出来)⇒ 截断恒在脱敏之后。
|
|
51
|
+
* `messages` 缺席 ⇒ 不铸键;非数组 ⇒ 深扫;其余键(`owner` / `promptEpoch` / `model` …)逐字。字节没变 ⇒ **同一引用**。
|
|
52
|
+
*/
|
|
53
|
+
export declare function redactSessionViewForRead<V extends object>(view: V): V;
|
|
43
54
|
/** Window `messages` — `tail=N` (last N) or `before=<offset>&limit=N` ([max(0,before-N), before)).
|
|
44
55
|
* Returns the window + its {offset,total} so the web can page upward. */
|
|
45
56
|
export declare function windowMessages(messages: unknown[], opts: {
|
package/dist/audit.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { StoredSession,
|
|
1
|
+
import { StoredSession, InMemorySessionStorage, boundedTail } from "@sema-agent/core";
|
|
2
2
|
import { TiDBSessionStorage } from "./plugins/tidb-session-storage.js";
|
|
3
3
|
import { PgSessionStorage } from "./plugins/pg-session-storage.js";
|
|
4
|
+
import { createLocalSessionRepo } from "./plugins/local-session-store.js";
|
|
5
|
+
import { redactModelText } from "./observability/run-terminal.js";
|
|
6
|
+
import { redactDeepWith } from "./trace/redact.js";
|
|
4
7
|
export async function buildSessionAudit(pool, sessionId) {
|
|
5
8
|
return auditFromStorage(await TiDBSessionStorage.wake(pool, sessionId), sessionId);
|
|
6
9
|
}
|
|
@@ -8,7 +11,7 @@ export async function buildSessionAuditPg(pool, sessionId) {
|
|
|
8
11
|
return auditFromStorage(await PgSessionStorage.wake(pool, sessionId), sessionId);
|
|
9
12
|
}
|
|
10
13
|
export async function buildSessionAuditLocal(root, sessionId) {
|
|
11
|
-
const repo =
|
|
14
|
+
const repo = createLocalSessionRepo(root);
|
|
12
15
|
let entries;
|
|
13
16
|
try {
|
|
14
17
|
entries = await repo.exportEntries(sessionId);
|
|
@@ -40,6 +43,63 @@ async function auditFromStorage(storage, sessionId) {
|
|
|
40
43
|
...(promptEpoch ? { promptEpoch } : {}),
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
const deepScrub = (v) => redactDeepWith(v, redactModelText);
|
|
47
|
+
const textSlot = (v) => (typeof v === "string" ? redactModelText(v) : deepScrub(v));
|
|
48
|
+
function scrubSlot(p, key, scan) {
|
|
49
|
+
const v = p[key];
|
|
50
|
+
if (v === undefined)
|
|
51
|
+
return p;
|
|
52
|
+
const next = scan(v);
|
|
53
|
+
return next === v ? p : { ...p, [key]: next };
|
|
54
|
+
}
|
|
55
|
+
const PART_READ_TREATMENT = {
|
|
56
|
+
text: (p) => scrubSlot(p, "text", textSlot),
|
|
57
|
+
thinking: (p) => scrubSlot(p, "thinking", textSlot),
|
|
58
|
+
toolCall: (p) => scrubSlot(p, "arguments", deepScrub),
|
|
59
|
+
image: (p) => p,
|
|
60
|
+
document: (p) => p,
|
|
61
|
+
};
|
|
62
|
+
function readParts(content) {
|
|
63
|
+
if (!Array.isArray(content))
|
|
64
|
+
return textSlot(content);
|
|
65
|
+
const next = content.map((part) => {
|
|
66
|
+
if (part !== null && typeof part === "object" && !Array.isArray(part)) {
|
|
67
|
+
const type = part.type;
|
|
68
|
+
if (typeof type === "string" && Object.hasOwn(PART_READ_TREATMENT, type)) {
|
|
69
|
+
return PART_READ_TREATMENT[type](part);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return deepScrub(part);
|
|
73
|
+
});
|
|
74
|
+
return next.every((x, i) => x === content[i]) ? content : next;
|
|
75
|
+
}
|
|
76
|
+
const readContent = (m) => scrubSlot(m, "content", readParts);
|
|
77
|
+
const MESSAGE_READ_TREATMENT = {
|
|
78
|
+
user: (m) => m,
|
|
79
|
+
custom: (m) => m,
|
|
80
|
+
assistant: (m) => readContent(m),
|
|
81
|
+
toolResult: (m) => scrubSlot(readContent(m), "details", deepScrub),
|
|
82
|
+
bashExecution: (m) => scrubSlot(scrubSlot(m, "command", textSlot), "output", textSlot),
|
|
83
|
+
compactionSummary: (m) => scrubSlot(m, "summary", textSlot),
|
|
84
|
+
};
|
|
85
|
+
function readMessage(m) {
|
|
86
|
+
if (m !== null && typeof m === "object" && !Array.isArray(m)) {
|
|
87
|
+
const role = m.role;
|
|
88
|
+
if (typeof role === "string" && Object.hasOwn(MESSAGE_READ_TREATMENT, role)) {
|
|
89
|
+
return MESSAGE_READ_TREATMENT[role](m);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return deepScrub(m);
|
|
93
|
+
}
|
|
94
|
+
export function redactSessionViewForRead(view) {
|
|
95
|
+
const { messages } = view;
|
|
96
|
+
if (messages === undefined)
|
|
97
|
+
return view;
|
|
98
|
+
const next = Array.isArray(messages)
|
|
99
|
+
? (() => { const out = messages.map(readMessage); return out.every((x, i) => x === messages[i]) ? messages : out; })()
|
|
100
|
+
: deepScrub(messages);
|
|
101
|
+
return next === messages ? view : { ...view, messages: next };
|
|
102
|
+
}
|
|
43
103
|
export function windowMessages(messages, opts) {
|
|
44
104
|
const total = messages.length;
|
|
45
105
|
if (opts.before !== undefined) {
|
package/dist/auto-mode-face.d.ts
CHANGED
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
* `entitlementSource`(既有位)说的是「center 源在不在」;`armed` 说的是「按三态谓词此刻会不会武装」。
|
|
43
43
|
* 两者刻意并存:前者告诉壳有没有组织面,后者告诉壳这台机器上 auto 到底是不是 default 的别名。
|
|
44
44
|
*/
|
|
45
|
-
import { type RunnerDeps, type RuntimeCaps } from "@sema-agent/core";
|
|
46
|
-
import type { SettingsPermissionMode } from "./task-settings.js";
|
|
45
|
+
import { type PermissionMode, type RunnerDeps, type RuntimeCaps } from "@sema-agent/core";
|
|
47
46
|
/**
|
|
48
47
|
* 分类器席的模型 —— **调 core 的单一解析器** `classifierSeatModel({deps, main})`(core 7.20.0 / [ref],
|
|
49
48
|
* 本仓 [ref] 的跨仓诉求兑现;契约句 `classifier.seat.one_resolver` 逐字:引擎的武装点与**显示这个席**
|
|
@@ -123,7 +122,7 @@ export type AutoModeVerdict = {
|
|
|
123
122
|
* 走到这里还在抛 = 装配缺陷,读面 500 比一句编出来的 reason 诚实。
|
|
124
123
|
*/
|
|
125
124
|
export declare function judgeAutoModeArming(face: AutoModeFaceDeps | undefined, input: {
|
|
126
|
-
readonly requestedMode:
|
|
125
|
+
readonly requestedMode: PermissionMode | undefined;
|
|
127
126
|
readonly principal: string | undefined;
|
|
128
127
|
/** 本地 `PERMISSIONS_DISABLE_AUTO_MODE`(= catalog/settings 键 `permissions.disableAutoMode`)是否置 true —— caps false 时分词
|
|
129
128
|
* `local_denied` / `org_denied`(seam ⑤;两者对壳的指路不同:改本机 env vs 找组织)。 */
|
package/dist/bake-runner/main.js
CHANGED
|
@@ -73,12 +73,7 @@ function makeApiClient(env, log) {
|
|
|
73
73
|
body: JSON.stringify(frame),
|
|
74
74
|
signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
|
|
75
75
|
});
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
accepted: res.ok && body.accepted !== false,
|
|
79
|
-
cancelRequested: body.cancelRequested === true,
|
|
80
|
-
leaseValid: res.status !== 409,
|
|
81
|
-
};
|
|
76
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
82
77
|
},
|
|
83
78
|
async heartbeat(bakeId, ingestSecret) {
|
|
84
79
|
const res = await fetch(`${env.imageApiBase}/v1/images/bakes/${encodeURIComponent(bakeId)}/ingest`, {
|
|
@@ -78,6 +78,11 @@ export declare function dockerBuildTickFrame(lineOrd: number, elapsedSec: number
|
|
|
78
78
|
export declare function synthDoneFrame(lineOrd: number, kind: "cancelled" | "timeout", detail?: {
|
|
79
79
|
error?: string;
|
|
80
80
|
}): IngestFrame;
|
|
81
|
+
/**
|
|
82
|
+
* 失败尾巴的行数 —— **唯一**常量:缓冲({@link createStderrTail})保留这么多行,终局句({@link exitDoneFrame})取这么多行。
|
|
83
|
+
* 两处读同一个数,所以缓冲里「保留的」恒等于消费方「要的」:既没有消费方想要而已被丢掉的行,也没有永远用不上的行占内存。
|
|
84
|
+
*/
|
|
85
|
+
export declare const STDERR_TAIL_LINES = 12;
|
|
81
86
|
/**
|
|
82
87
|
* The runner-side terminal `done` for a build.sh CHILD that exited non-zero WITHOUT printing its own `done` (build.sh
|
|
83
88
|
* normally dies via `die()` which DOES print a `done:FAILED`, but a SIGKILL or a crash before the trap can leave the
|
|
@@ -87,6 +92,19 @@ export declare function synthDoneFrame(lineOrd: number, kind: "cancelled" | "tim
|
|
|
87
92
|
export declare function exitDoneFrame(lineOrd: number, exitCode: number, stderrTail: string): IngestFrame;
|
|
88
93
|
/** Keep only the last `n` lines of a (possibly long) stderr tail — bounds the error string we forward. */
|
|
89
94
|
export declare function lastLines(text: string, n: number): string;
|
|
95
|
+
/** 有界的 stderr 尾部缓冲:只保留**末** {@link STDERR_TAIL_LINES} 行(环形:新行进来、最老的出去)。 */
|
|
96
|
+
export interface StderrTail {
|
|
97
|
+
/** 收一行(spawner 已按换行切好;这里仍按 {@link contentLines} 判据过一遍,空行不入)。 */
|
|
98
|
+
push(raw: string): void;
|
|
99
|
+
/** 当前保留的行,`\n` 连接;空缓冲 = 空串。可直接交 {@link exitDoneFrame}。 */
|
|
100
|
+
text(): string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 修前缓冲是 `if (stderrTail.length < 80) push` —— 留的是**首** 80 行,而消费方({@link exitDoneFrame})要的是**末** 12 行:
|
|
104
|
+
* 超过 80 行的失败里,「尾巴」是前 80 行的末 12 行,真正的最后一行(失败原因所在)一个字也进不来。
|
|
105
|
+
* 现在留末 N 行;N = {@link STDERR_TAIL_LINES}。
|
|
106
|
+
*/
|
|
107
|
+
export declare function createStderrTail(): StderrTail;
|
|
90
108
|
/**
|
|
91
109
|
* The profile-aware HARD DEADLINE (§P2.7/§P2.11) the runner enforces by killing the process group. flutter/full are
|
|
92
110
|
* the heavy profiles (~60min); everything else ~45min. A bands build ("custom") is treated as heavy if it includes
|