@sema-agent/server 7.135.0 → 7.137.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 +237 -2
- package/USAGE.md +146 -41
- package/deploy/sema-up/README.md +15 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +7 -1
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +7 -1
- package/dist/approval-card.d.ts +60 -4
- package/dist/approval-card.js +18 -1
- package/dist/approval-content-kind.d.ts +7 -0
- package/dist/approval-content-kind.js +8 -1
- package/dist/approval-gate-currency.d.ts +48 -6
- package/dist/approval-gate-currency.js +16 -9
- package/dist/approval-hmac.d.ts +13 -1
- package/dist/approval-hmac.js +1 -1
- package/dist/approval.js +3 -3
- package/dist/auth-bridge.d.ts +34 -5
- package/dist/auth-bridge.js +22 -5
- package/dist/boot/budget-tracing.d.ts +1 -2
- package/dist/boot/budget-tracing.js +2 -2
- package/dist/boot/execution-env.d.ts +24 -1
- package/dist/boot/execution-env.js +69 -6
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +15 -1
- package/dist/boot/resolve-spec.d.ts +4 -0
- package/dist/boot/resolve-spec.js +24 -22
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-04-stores.d.ts +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +4 -2
- package/dist/boot/stage-05-execution-env.js +11 -2
- package/dist/boot/stage-06-runners.d.ts +4 -2
- package/dist/boot/stage-06-runners.js +5 -1
- package/dist/boot/stage-07-capability-layer.d.ts +5 -2
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +5 -2
- package/dist/boot/stage-08-reapers.js +2 -1
- package/dist/boot/stage-09-leader.d.ts +5 -2
- package/dist/boot/stage-10-http-server.d.ts +5 -2
- package/dist/boot/stage-10-http-server.js +3 -2
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +2 -3
- package/dist/boot/web-admin.js +10 -17
- package/dist/brain.js +12 -3
- package/dist/broker/broker.d.ts +89 -0
- package/dist/broker/broker.js +287 -0
- package/dist/broker/constants.d.ts +39 -0
- package/dist/broker/constants.js +15 -0
- package/dist/broker/http1.d.ts +111 -0
- package/dist/broker/http1.js +256 -0
- package/dist/broker/lease-book.d.ts +87 -0
- package/dist/broker/lease-book.js +142 -0
- package/dist/broker/lease-file.d.ts +60 -0
- package/dist/broker/lease-file.js +127 -0
- package/dist/broker/main.d.ts +2 -0
- package/dist/broker/main.js +20 -0
- package/dist/budget.d.ts +3 -3
- package/dist/budget.js +4 -0
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/engine-capabilities.js +1 -0
- package/dist/capabilities/mcp-probe.d.ts +5 -4
- package/dist/capabilities/scenarios.d.ts +23 -8
- package/dist/capabilities/scenarios.js +13 -8
- package/dist/capabilities/skills.d.ts +28 -5
- package/dist/capabilities/skills.js +8 -0
- package/dist/capability-facts.d.ts +3 -7
- package/dist/capability-facts.js +3 -5
- package/dist/center-credential-renewer.js +5 -1
- package/dist/center-credential.d.ts +18 -4
- package/dist/center-credential.js +18 -3
- package/dist/checkpoint-ctx-seal-m28.d.ts +3 -10
- package/dist/checkpoint-ctx-seal-m28.js +1 -1
- package/dist/checkpoint-identity.d.ts +3 -3
- package/dist/config-catalog.js +21 -16
- package/dist/config-d-migration.js +1 -1
- package/dist/config-invariants.d.ts +1 -1
- package/dist/config-invariants.js +8 -0
- package/dist/config-lkg.d.ts +0 -38
- package/dist/config-lkg.js +1 -83
- package/dist/config-types.d.ts +21 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +60 -16
- package/dist/deployment-governance.d.ts +2 -2
- package/dist/deployment-governance.js +7 -9
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/elicitation.d.ts +8 -7
- package/dist/elicitation.js +9 -8
- package/dist/file-history-disclosure.d.ts +8 -4
- package/dist/file-history-disclosure.js +24 -6
- package/dist/fleet-client.d.ts +32 -1
- package/dist/fleet-client.js +24 -4
- package/dist/fleet-lease.d.ts +7 -3
- package/dist/fleet-lease.js +31 -23
- package/dist/hooks/classic-stations.d.ts +11 -54
- package/dist/hooks/hook-llm.js +14 -8
- package/dist/hooks/hook-runner.d.ts +4 -4
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/admission.js +10 -7
- package/dist/http/approval-sweeps.js +4 -3
- package/dist/http/caller-trust.d.ts +2 -1
- package/dist/http/caller-trust.js +1 -3
- package/dist/http/checkpoint-ctx.d.ts +76 -16
- package/dist/http/checkpoint-ctx.js +45 -1
- package/dist/http/dispatch.js +24 -19
- package/dist/http/durable-run-leg.d.ts +5 -1
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/health-self-report.d.ts +2 -2
- package/dist/http/mcp-remote-text.d.ts +5 -0
- package/dist/http/mcp-remote-text.js +7 -0
- package/dist/http/principal-gate.d.ts +15 -10
- package/dist/http/principal-gate.js +48 -9
- package/dist/http/resume-ctx-read.d.ts +2 -16
- package/dist/http/resume-ctx-read.js +2 -3
- package/dist/http/resume-legs.d.ts +2 -20
- package/dist/http/resume-legs.js +52 -38
- package/dist/http/route-ctx.d.ts +8 -2
- package/dist/http/routes/approvals-assistant.d.ts +0 -2
- package/dist/http/routes/approvals-assistant.js +65 -16
- package/dist/http/routes/capabilities.js +11 -1
- package/dist/http/routes/mcp-reconnect.d.ts +1 -1
- package/dist/http/routes/mcp-reconnect.js +5 -7
- package/dist/http/routes/runs.js +4 -3
- package/dist/http/routes/session-sync.js +70 -12
- package/dist/http/routes/sessions.js +20 -15
- package/dist/http/routes/tasks.js +8 -6
- package/dist/http/server.d.ts +4 -0
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +6 -1
- package/dist/http/submit-ctx.js +1 -1
- package/dist/http/wire-types.d.ts +57 -30
- package/dist/identity-source.d.ts +65 -0
- package/dist/identity-source.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory-store-refusal.d.ts +3 -3
- package/dist/model-entry-refusal.d.ts +2 -0
- package/dist/model-entry-refusal.js +11 -0
- package/dist/model-route-gap.d.ts +49 -4
- package/dist/model-route-gap.js +34 -1
- package/dist/model-select.d.ts +6 -17
- package/dist/model-select.js +0 -3
- package/dist/namespaced-rule-name.d.ts +1 -1
- package/dist/object-store-buckets.d.ts +13 -0
- package/dist/object-store-buckets.js +3 -0
- package/dist/observability/fail-open.d.ts +19 -7
- package/dist/observability/fail-open.js +19 -7
- package/dist/observability/metrics.js +2 -2
- package/dist/observability/principal-context.d.ts +4 -1
- package/dist/observability/principal-context.js +6 -1
- package/dist/observability/run-terminal.js +1 -0
- package/dist/operator-ask.d.ts +2 -2
- package/dist/org-delivery-gate.js +1 -1
- package/dist/org-memory-admission.d.ts +2 -2
- package/dist/org-memory-admission.js +39 -2
- package/dist/parked-decide.d.ts +4 -3
- package/dist/parked-decide.js +29 -9
- package/dist/peer-directory.d.ts +2 -1
- package/dist/peer-directory.js +7 -5
- package/dist/permission-rule-list.d.ts +2 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
- package/dist/plugins/approval-ask-store-sql.js +3 -0
- package/dist/plugins/blob-backend.js +2 -1
- package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
- package/dist/plugins/checkpoint-index-columns.js +28 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +46 -12
- package/dist/plugins/checkpoint-store-sql.js +64 -35
- package/dist/plugins/file-history-store-sql.d.ts +6 -0
- package/dist/plugins/file-history-store-sql.js +37 -37
- package/dist/plugins/file-run-store.js +8 -2
- package/dist/plugins/k8s-exec-protocol.d.ts +69 -13
- package/dist/plugins/k8s-exec-protocol.js +116 -28
- package/dist/plugins/k8s-pod-delete-failure.d.ts +25 -0
- package/dist/plugins/k8s-pod-delete-failure.js +17 -0
- package/dist/plugins/k8s-pod-reaper.d.ts +112 -0
- package/dist/plugins/k8s-pod-reaper.js +122 -0
- package/dist/plugins/local-checkpoint-store.d.ts +3 -6
- package/dist/plugins/local-checkpoint-store.js +7 -6
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/one-time-migrations.d.ts +49 -37
- package/dist/plugins/one-time-migrations.js +87 -128
- package/dist/plugins/permission-rule-store-file.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -17
- package/dist/plugins/permission-rule-store-sql.js +16 -17
- package/dist/plugins/pg-pool.js +4 -0
- package/dist/plugins/pg-session-storage.js +2 -2
- package/dist/plugins/remote-env-adb.d.ts +3 -0
- package/dist/plugins/remote-env-adb.js +1 -0
- package/dist/plugins/remote-env-device.d.ts +3 -0
- package/dist/plugins/remote-env-device.js +4 -0
- package/dist/plugins/remote-env-e2b.d.ts +3 -0
- package/dist/plugins/remote-env-e2b.js +1 -0
- package/dist/plugins/remote-env-file-error.d.ts +8 -1
- package/dist/plugins/remote-env-file-error.js +7 -1
- package/dist/plugins/remote-env-host.d.ts +7 -1
- package/dist/plugins/remote-env-host.js +7 -1
- package/dist/plugins/remote-env-k8s.d.ts +154 -14
- package/dist/plugins/remote-env-k8s.js +300 -21
- package/dist/plugins/remote-env-local-docker.d.ts +3 -0
- package/dist/plugins/remote-env-local-docker.js +1 -0
- package/dist/plugins/remote-env-ssh.d.ts +3 -0
- package/dist/plugins/remote-env-ssh.js +1 -0
- package/dist/plugins/run-store-sql.js +10 -4
- package/dist/plugins/s3-answer.d.ts +15 -0
- package/dist/plugins/s3-answer.js +5 -0
- package/dist/plugins/session-ownership.d.ts +17 -3
- package/dist/plugins/session-ownership.js +19 -5
- package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
- package/dist/plugins/snapshot-bucket-presence.js +35 -7
- package/dist/plugins/sql-errors.d.ts +60 -0
- package/dist/plugins/sql-errors.js +64 -1
- package/dist/plugins/store-contracts.d.ts +10 -1
- package/dist/plugins/store-fault.d.ts +4 -0
- package/dist/plugins/store-fault.js +10 -0
- package/dist/plugins/tidb-pool.js +8 -1
- package/dist/plugins/tidb-session-store.js +2 -2
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/plugins/web-search.js +2 -2
- package/dist/request-declaration-refusal.d.ts +94 -0
- package/dist/request-declaration-refusal.js +78 -0
- package/dist/request-image-urls.d.ts +6 -0
- package/dist/request-image-urls.js +27 -0
- package/dist/request-key-closure.d.ts +5 -0
- package/dist/request-key-closure.js +5 -2
- package/dist/request-value-closure.d.ts +31 -2
- package/dist/request-value-closure.js +7 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +2 -2
- package/dist/rules-consent.js +7 -4
- package/dist/run-local.d.ts +2 -2
- package/dist/runs.js +2 -1
- package/dist/runtime-caps-resolver.js +1 -1
- package/dist/runtime-governance.d.ts +5 -5
- package/dist/sandbox-git-lease.d.ts +49 -0
- package/dist/sandbox-git-lease.js +60 -0
- package/dist/sandbox-git-plane.d.ts +84 -0
- package/dist/sandbox-git-plane.js +105 -0
- package/dist/sandbox-pkg-source.d.ts +28 -1
- package/dist/sandbox-pkg-source.js +1 -2
- package/dist/sealed-key.d.ts +19 -0
- package/dist/sealed-key.js +47 -4
- package/dist/security.d.ts +16 -4
- package/dist/security.js +8 -3
- package/dist/server-secret-env.d.ts +6 -0
- package/dist/server-secret-env.js +14 -0
- package/dist/session-error-wire.d.ts +14 -6
- package/dist/session-error-wire.js +2 -1
- package/dist/session-incarnation.d.ts +16 -5
- package/dist/session-incarnation.js +7 -4
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-policy-wire.js +2 -2
- package/dist/session-row.d.ts +3 -17
- package/dist/session-row.js +4 -0
- package/dist/session-sync.d.ts +15 -3
- package/dist/session-sync.js +5 -2
- package/dist/spec-fields.d.ts +43 -13
- package/dist/spec-fields.js +33 -12
- package/dist/stored-request-prompt-profile-m30.d.ts +49 -0
- package/dist/stored-request-prompt-profile-m30.js +25 -0
- package/dist/task-a2a.d.ts +52 -81
- package/dist/task-a2a.js +219 -89
- package/dist/task-mcp.d.ts +7 -15
- package/dist/task-mcp.js +16 -51
- package/dist/task-settings.d.ts +2 -2
- package/dist/tool-approval.d.ts +35 -12
- package/dist/tool-approval.js +59 -39
- package/dist/tool-rule-names.d.ts +5 -0
- package/dist/trace/core-keyset-guard.d.ts +5 -5
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/exit-seat.js +37 -13
- package/dist/trace/ledger-sink.d.ts +4 -5
- package/dist/trace/ledger-sink.js +3 -7
- package/dist/trace/project.d.ts +37 -3
- package/dist/trace/project.js +20 -6
- package/dist/trace/redact.d.ts +40 -12
- package/dist/trace/redact.js +332 -128
- package/dist/trace/self-minted-wire.d.ts +641 -0
- package/dist/trace/self-minted-wire.js +201 -0
- package/dist/trace/wire-projection-faces.d.ts +5 -3
- package/dist/trace/wire-projection-faces.js +7 -3
- package/dist/url-credential-refusal.d.ts +52 -0
- package/dist/url-credential-refusal.js +14 -0
- package/dist/url-refusal.d.ts +13 -4
- package/dist/url-refusal.js +8 -2
- package/dist/web-admin-client/apply-effective.js +0 -1
- package/dist/web-admin-client/center-request.d.ts +20 -2
- package/dist/web-admin-client/center-request.js +40 -5
- package/dist/web-admin-client/http-client.js +6 -6
- package/dist/web-admin-client/types.d.ts +5 -5
- package/dist/workspace-space.d.ts +27 -0
- package/dist/workspace-space.js +26 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +11 -5
- package/package.json +4 -3
- package/scripts/drain-check-local.mjs +2 -1
- package/scripts/drain-check.mysql.sql +3 -1
- package/scripts/drain-check.pg.sql +3 -1
- package/skills/schedule.md +1 -1
- package/wire-changes/server@7.104.0.json +134 -0
- package/wire-changes/server@7.105.0.json +53 -0
- package/wire-changes/server@7.106.0.json +475 -0
- package/wire-changes/server@7.107.0.json +64 -0
- package/wire-changes/server@7.108.0.json +153 -0
- package/wire-changes/server@7.109.0.json +46 -0
- package/wire-changes/server@7.110.0.json +90 -0
- package/wire-changes/server@7.111.0.json +86 -0
- package/wire-changes/server@7.112.0.json +60 -0
- package/wire-changes/server@7.113.0.json +26 -0
- package/wire-changes/server@7.114.0.json +26 -0
- package/wire-changes/server@7.115.0.json +97 -0
- package/wire-changes/server@7.116.0.json +218 -0
- package/wire-changes/server@7.116.1.json +26 -0
- package/wire-changes/server@7.117.0.json +65 -0
- package/wire-changes/server@7.118.0.json +129 -0
- package/wire-changes/server@7.119.0.json +65 -0
- package/wire-changes/server@7.120.0.json +89 -0
- package/wire-changes/server@7.121.0.json +219 -0
- package/wire-changes/server@7.122.0.json +54 -0
- package/wire-changes/server@7.123.0.json +26 -0
- package/wire-changes/server@7.124.0.json +336 -0
- package/wire-changes/server@7.124.1.json +26 -0
- package/wire-changes/server@7.125.0.json +111 -0
- package/wire-changes/server@7.126.0.json +285 -0
- package/wire-changes/server@7.127.0.json +77 -0
- package/wire-changes/server@7.127.1.json +26 -0
- package/wire-changes/server@7.127.2.json +53 -0
- package/wire-changes/server@7.128.0.json +26 -0
- package/wire-changes/server@7.129.0.json +106 -0
- package/wire-changes/server@7.129.1.json +26 -0
- package/wire-changes/server@7.130.0.json +26 -0
- package/wire-changes/server@7.131.0.json +118 -0
- package/wire-changes/server@7.132.0.json +226 -0
- package/wire-changes/server@7.133.0.json +1155 -0
- package/wire-changes/server@7.134.0.json +107 -0
- package/wire-changes/server@7.135.0.json +174 -0
- package/wire-changes/server@7.136.0.json +448 -0
- package/wire-changes/server@7.137.0.json +825 -0
- package/dist/url-credential.d.ts +0 -52
- package/dist/url-credential.js +0 -49
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
2
|
+
import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
|
|
3
|
+
import { SANDBOX_SECRET_SEAT_VERBS } from "./plugins/k8s-exec-protocol.js";
|
|
4
|
+
export const SANDBOX_GIT_PLANE_OFF_REASONS = [
|
|
5
|
+
"single_user",
|
|
6
|
+
"lane_without_boundary",
|
|
7
|
+
"no_caller_credential",
|
|
8
|
+
"center_not_https",
|
|
9
|
+
"secret_seat_unverified",
|
|
10
|
+
"secret_seat_denied",
|
|
11
|
+
"pod_patch_reaches_broker",
|
|
12
|
+
"broker_image_unset",
|
|
13
|
+
"no_origins",
|
|
14
|
+
];
|
|
15
|
+
export const SANDBOX_GIT_PLANE_OFF_TEXT = {
|
|
16
|
+
single_user: "this is a single-user deployment (REQUIRE_PRINCIPAL is not true): there are no members to keep apart",
|
|
17
|
+
lane_without_boundary: "the execution lane is not the k8s pod sandbox (REMOTE_EXEC=k8s): only a k8s sandbox pod can carry the credential broker beside the agent",
|
|
18
|
+
no_caller_credential: "this worker holds no credential the console's vault admits for credential resolution",
|
|
19
|
+
center_not_https: `the console address (${WEB_ADMIN_ENV.url}) is not https — credentials are never fetched over plain http`,
|
|
20
|
+
secret_seat_unverified: `the boot could not confirm the sandbox Role's secrets verbs for the lease Secret (${SANDBOX_SECRET_SEAT_VERBS.lease.join(" / ")}) in the sandbox namespace`,
|
|
21
|
+
secret_seat_denied: `the sandbox Role does not allow secrets ${SANDBOX_SECRET_SEAT_VERBS.lease.join(" / ")} (the lease Secret) in the sandbox namespace`,
|
|
22
|
+
pod_patch_reaches_broker: "the deployment's pod-spec patch could reach the broker container (a podSecurityContext, or a field outside the broker white-list)",
|
|
23
|
+
broker_image_unset: "K8S_SANDBOX_BROKER_IMAGE is not set (the broker container's image; this engine's own image is the recommended value)",
|
|
24
|
+
no_origins: "the effective configuration routes no git origins to the plane (sandboxGit.origins)",
|
|
25
|
+
};
|
|
26
|
+
const isHttps = (url) => url !== undefined && URL.canParse(url) && new URL(url).protocol === "https:";
|
|
27
|
+
export function judgeSandboxGitPlane(p) {
|
|
28
|
+
const off = (reason) => ({ state: "off", reason });
|
|
29
|
+
if (!p.multiTenant)
|
|
30
|
+
return off("single_user");
|
|
31
|
+
if (!p.k8sLane)
|
|
32
|
+
return off("lane_without_boundary");
|
|
33
|
+
if (!p.callerCredential)
|
|
34
|
+
return off("no_caller_credential");
|
|
35
|
+
if (!isHttps(p.centerUrl))
|
|
36
|
+
return off("center_not_https");
|
|
37
|
+
const seat = p.secretSeat;
|
|
38
|
+
if (seat === undefined)
|
|
39
|
+
return off("secret_seat_unverified");
|
|
40
|
+
switch (seat.state) {
|
|
41
|
+
case "unverified":
|
|
42
|
+
return off("secret_seat_unverified");
|
|
43
|
+
case "denied":
|
|
44
|
+
return off("secret_seat_denied");
|
|
45
|
+
case "granted":
|
|
46
|
+
break;
|
|
47
|
+
default: {
|
|
48
|
+
const unreachable = seat;
|
|
49
|
+
throw new Error(`unhandled sandbox secret seat reading: ${String(unreachable.state)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (p.patchRefusal !== undefined)
|
|
53
|
+
return off("pod_patch_reaches_broker");
|
|
54
|
+
if (p.brokerImage === undefined || p.brokerImage === "")
|
|
55
|
+
return off("broker_image_unset");
|
|
56
|
+
if (p.origins === null || p.origins.length === 0)
|
|
57
|
+
return off("no_origins");
|
|
58
|
+
return {
|
|
59
|
+
state: "on",
|
|
60
|
+
origins: p.origins.map((origin) => (origin.startsWith("https://") ? { origin, delivers: true } : { origin, delivers: false, reason: "insecure_transport" })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function sandboxGitPlaneAsksSecretSeat(p) {
|
|
64
|
+
return p.multiTenant && p.k8sLane && p.callerCredential && isHttps(p.centerUrl);
|
|
65
|
+
}
|
|
66
|
+
export function sandboxGitPlanePremisesOf(config, readings) {
|
|
67
|
+
const lane = config.remoteExec;
|
|
68
|
+
return {
|
|
69
|
+
multiTenant: DEPLOYMENT_SHAPE_PREDICATES.multiTenant.holds(config),
|
|
70
|
+
k8sLane: lane?.provider === "k8s",
|
|
71
|
+
callerCredential: readSandboxGitCallerCredential(config),
|
|
72
|
+
centerUrl: config.webAdmin?.baseUrl,
|
|
73
|
+
secretSeat: readings.secretSeat,
|
|
74
|
+
patchRefusal: readings.patchRefusal,
|
|
75
|
+
brokerImage: lane?.provider === "k8s" ? lane.brokerImage : undefined,
|
|
76
|
+
origins: readSandboxGitConfig(config)?.origins ?? null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function readSandboxGitConfig(_config) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
export function readSandboxGitCallerCredential(_config) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
export async function runIdentitySourceOf(runId, runs, logger) {
|
|
86
|
+
if (runId === undefined || runs === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
try {
|
|
89
|
+
return (await runs.getRun(runId))?.identity?.source;
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
logger?.warn("sandbox_broker_identity_unread", {
|
|
93
|
+
run: runId,
|
|
94
|
+
error: e instanceof Error ? e.message : String(e),
|
|
95
|
+
effect: "this run's sandbox pod carries no member-credential broker (its identity could not be read from the run row)",
|
|
96
|
+
});
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function sandboxBrokerSeatOf(plane, brokerImage, identitySource) {
|
|
101
|
+
if (plane.state !== "on" || brokerImage === undefined || brokerImage === "" || identitySource !== "sso")
|
|
102
|
+
return undefined;
|
|
103
|
+
return { image: brokerImage, origins: plane.origins.map((o) => o.origin) };
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=sandbox-git-plane.js.map
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* RFC B5/B4 抽象点② (sema-internal server/docs/design/ENV-SELECT-AND-REGION-SOURCE.md): the deployment-region package-source →
|
|
3
|
+
* sandbox env derivation. ENV-FIRST: pod/sandbox env OVERRIDES the image's baked `ENV` (k8s container env wins
|
|
4
|
+
* over image config and is visible to every exec'd process; e2b sandboxEnv likewise) — so the env-overridable
|
|
5
|
+
* ecosystem pieces (pip/uv/npm/corepack/go/rustup/flutter/playwright/nuget) switch region on EXISTING images,
|
|
6
|
+
* no rebake needed. Only the file-bound pieces (apt sources, /etc/gemrc, composer global config, cargo
|
|
7
|
+
* config.toml) still need the in-image `pkg-source.sh` hook (rebaked images; bake prep chowns those files).
|
|
8
|
+
*
|
|
9
|
+
* Profiles:
|
|
10
|
+
* cn → inject only the SEMA_PKG_SOURCE marker (the pool's images bake the CN set as their defaults).
|
|
11
|
+
* global → official endpoints for every env-overridable ecosystem (overrides the baked CN ENV values).
|
|
12
|
+
* custom → SEMA_* explicit URLs (from the service process env) forwarded verbatim + mapped onto the same
|
|
13
|
+
* ecosystem variables where a mapping is unambiguous (apt has no env form — hook-only).
|
|
14
|
+
*
|
|
15
|
+
* The marker `SEMA_PKG_SOURCE` always rides along: the image's pkg-source.sh hook (pod-start / profile.d)
|
|
16
|
+
* materializes the file-bound pieces from it; images without the hook simply ignore it.
|
|
17
|
+
*
|
|
18
|
+
* ## One law: no URL that rides into the model-visible sandbox env carries a credential
|
|
19
|
+
* Everything this module derives lands in the env of every sandbox process — including the ones the model drives
|
|
20
|
+
* (`env`, `printenv`) — so `https://user:pass@mirror/…` or `https://mirror/simple?token=…` among the `custom` addresses would
|
|
21
|
+
* hand the deployment's package-source credential to the model. {@link customPkgSourceFromEnv} is the ONE reader of the six
|
|
22
|
+
* addresses and the one place that judges them, in two questions: the shape (the shared URL-shape judge `httpUrlFlaws`, the same
|
|
23
|
+
* one MEMORY_EMBEDDER_ENDPOINT / A2A_SERVE_URL answer to, plus this seat's backslash rule) and the credential (the shared
|
|
24
|
+
* credential judge `findUrlCredential` — userinfo, or a query / fragment parameter named in the log redactor's credential-name
|
|
25
|
+
* table; the request-face MCP `transport.url` answers to the same judge). Either says no ⇒ it throws, startup refuses. Never
|
|
26
|
+
* stripped, never rewritten, never forwarded quietly. The request face has no package-source address key at all (deployment face only).
|
|
27
|
+
*/
|
|
28
|
+
import { type UrlCredential } from "@sema-agent/settings-schema";
|
|
2
29
|
/** Explicit URLs for `custom` (RFC B: 极端情况出口). Read from the service process env (operator-trusted). */
|
|
3
30
|
export interface CustomPkgSource {
|
|
4
31
|
aptMirror?: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { httpUrlFlaws } from "@sema-agent/settings-schema";
|
|
1
|
+
import { findUrlCredential, httpUrlFlaws } from "@sema-agent/settings-schema";
|
|
2
2
|
import { buildHttpUrlRefusal, buildUrlCredentialRefusal } from "./url-refusal.js";
|
|
3
|
-
import { findUrlCredential } from "./url-credential.js";
|
|
4
3
|
const GLOBAL_ENV = {
|
|
5
4
|
PIP_INDEX_URL: "https://pypi.org/simple",
|
|
6
5
|
UV_DEFAULT_INDEX: "https://pypi.org/simple",
|
package/dist/sealed-key.d.ts
CHANGED
|
@@ -54,6 +54,25 @@ export declare function projectSealedKeyCapability(face: SealedKeyPublicFace | u
|
|
|
54
54
|
/** Default id = fingerprint of the RAW public-key bytes: `exec-<sha256(pk)[0..12) hex>`.
|
|
55
55
|
* Deterministic → regenerating the id from the same key file is stable across boots. */
|
|
56
56
|
export declare function deriveKeyId(publicKeyRaw: Uint8Array): string;
|
|
57
|
+
/** {@link judgeSealedKeyPublicKey} 的拒因(闭集)。 */
|
|
58
|
+
export type SealedKeyPublicKeyFlaw = "non_canonical_base64" | "bad_length" | "small_order";
|
|
59
|
+
/** 公钥判官的拒(类型化;`flaw` 闭集)。公钥是公开材料,句子里点名它无妨;从不带私钥 / 密文。 */
|
|
60
|
+
export declare class SealedKeyPublicKeyRefusedError extends Error {
|
|
61
|
+
readonly flaw: SealedKeyPublicKeyFlaw;
|
|
62
|
+
constructor(flaw: SealedKeyPublicKeyFlaw);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* **X25519 公钥的唯一判官** —— 保管店装载({@link ensureSealedKeyStore},每份钥匙文件)、向中心登记({@link reportExecutionPublicKey})、
|
|
66
|
+
* 封之前({@link createCheckpointCtxSealing},对着最新那把)三处同读这一只;过了交回原始 32 字节,不过抛 {@link SealedKeyPublicKeyRefusedError}。
|
|
67
|
+
* 1. **规范 base64**:解出来的字节再编码回去必须逐字等于原串 —— 一句话盖住字母表、补齐、填充位三件(一把钥匙只有一种写法);
|
|
68
|
+
* 2. **32 字节**;
|
|
69
|
+
* 3. **不是小阶点**:对 {@link SMALL_ORDER_PROBE_SCALAR} 做 `crypto_scalarmult`,结果全零 ⇔ 小阶(libsodium 此时不交结果、抛 `weak public key`)。
|
|
70
|
+
* 判法取「标量乘结果全零」而不是手抄小阶点表:与 noble 同一判(`x25519.getSharedSecret` 零结果即抛,包侧 `bad_public_key` 读的就是它)、
|
|
71
|
+
* 就是 RFC 7748 §6.1 要求的那一检;非规范编码(p、p+1)与第 255 位置位的写法由 X25519 自己的约化 / 掩位盖住,不必逐个列;
|
|
72
|
+
* 高位别名(第 255 位置位、掩掉后是同一个非小阶点)照收 —— 两边同口径。依赖已在树里(libsodium),不新增一张要人维护的表。
|
|
73
|
+
* 前置:libsodium 已就绪(三处调用方都先等过它);没就绪 ⇒ 当场抛普通错误(不是判决)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function judgeSealedKeyPublicKey(publicKeyB64: string): Uint8Array;
|
|
57
76
|
/**
|
|
58
77
|
* Load every keypair under `dir` (creating dir + ONE fresh keypair when none exist) and return the
|
|
59
78
|
* custody store. Invalid/corrupt files are skipped with a warn (they may be an OLD format or a
|
package/dist/sealed-key.js
CHANGED
|
@@ -27,6 +27,37 @@ function b64ToBytes(b64, what) {
|
|
|
27
27
|
throw new Error(`${what}: not valid base64`);
|
|
28
28
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
29
29
|
}
|
|
30
|
+
export class SealedKeyPublicKeyRefusedError extends Error {
|
|
31
|
+
flaw;
|
|
32
|
+
constructor(flaw) {
|
|
33
|
+
super(`sealed-key public key refused (${flaw}): ${SEALED_KEY_PUBLIC_KEY_FLAW_TEXT[flaw]}`);
|
|
34
|
+
this.name = "SealedKeyPublicKeyRefusedError";
|
|
35
|
+
this.flaw = flaw;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const SEALED_KEY_PUBLIC_KEY_FLAW_TEXT = {
|
|
39
|
+
non_canonical_base64: "not the canonical standard-base64 spelling of 32 bytes (alphabet A-Z a-z 0-9 + /, `=` padding, zero padding bits)",
|
|
40
|
+
bad_length: "does not decode to 32 bytes",
|
|
41
|
+
small_order: "an X25519 small-order point — anything sealed against it can be opened without the private key",
|
|
42
|
+
};
|
|
43
|
+
const SMALL_ORDER_PROBE_SCALAR = new Uint8Array(32).fill(7);
|
|
44
|
+
export function judgeSealedKeyPublicKey(publicKeyB64) {
|
|
45
|
+
const sodium = _sodium;
|
|
46
|
+
if (typeof sodium.crypto_scalarmult !== "function")
|
|
47
|
+
throw new Error("judgeSealedKeyPublicKey: libsodium is not initialized — await its ready promise first");
|
|
48
|
+
const bytes = new Uint8Array(Buffer.from(publicKeyB64, "base64"));
|
|
49
|
+
if (Buffer.from(bytes).toString("base64") !== publicKeyB64)
|
|
50
|
+
throw new SealedKeyPublicKeyRefusedError("non_canonical_base64");
|
|
51
|
+
if (bytes.length !== sodium.crypto_box_PUBLICKEYBYTES)
|
|
52
|
+
throw new SealedKeyPublicKeyRefusedError("bad_length");
|
|
53
|
+
try {
|
|
54
|
+
sodium.crypto_scalarmult(SMALL_ORDER_PROBE_SCALAR, bytes);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new SealedKeyPublicKeyRefusedError("small_order");
|
|
58
|
+
}
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
30
61
|
export async function ensureSealedKeyStore(dir, logger) {
|
|
31
62
|
await _sodium.ready;
|
|
32
63
|
const sodium = _sodium;
|
|
@@ -40,9 +71,9 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
40
71
|
if (raw.alg !== SEALED_KEY_ALG || typeof raw.publicKey !== "string" || typeof raw.privateKey !== "string" || typeof raw.publicKeyId !== "string") {
|
|
41
72
|
throw new Error("missing/unsupported fields");
|
|
42
73
|
}
|
|
43
|
-
const publicKey =
|
|
74
|
+
const publicKey = judgeSealedKeyPublicKey(raw.publicKey);
|
|
44
75
|
const privateKey = b64ToBytes(raw.privateKey, "privateKey");
|
|
45
|
-
if (
|
|
76
|
+
if (privateKey.length !== sodium.crypto_box_SECRETKEYBYTES)
|
|
46
77
|
throw new Error("bad key length");
|
|
47
78
|
if (name !== `${raw.publicKeyId}.json`)
|
|
48
79
|
logger?.warn("sealed_key_filename_drift", { file: name, publicKeyId: raw.publicKeyId, note: "content publicKeyId wins; rename the file to silence this" });
|
|
@@ -53,7 +84,7 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
53
84
|
keys.set(raw.publicKeyId, { publicKey, privateKey, publicKeyB64: raw.publicKey, createdAt: typeof raw.createdAt === "string" ? raw.createdAt : "" });
|
|
54
85
|
}
|
|
55
86
|
catch (err) {
|
|
56
|
-
logger?.warn("sealed_key_file_unreadable", { file: path, errKind: err instanceof Error ? err.name : "unknown", note: "skipped (NOT deleted) — ciphertexts sealed for this key will fail loud per-model" });
|
|
87
|
+
logger?.warn("sealed_key_file_unreadable", { file: path, errKind: err instanceof Error ? err.name : "unknown", ...(err instanceof SealedKeyPublicKeyRefusedError ? { flaw: err.flaw } : {}), note: "skipped (NOT deleted) — ciphertexts sealed for this key will fail loud per-model" });
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
if (keys.size === 0) {
|
|
@@ -101,7 +132,7 @@ export function createCheckpointCtxSealing(store) {
|
|
|
101
132
|
if (typeof sodium.crypto_box_seal !== "function")
|
|
102
133
|
throw new Error("createCheckpointCtxSealing: libsodium is not initialized — build the store with ensureSealedKeyStore() first");
|
|
103
134
|
const { publicKeyId } = store.latest;
|
|
104
|
-
const publicKey =
|
|
135
|
+
const publicKey = judgeSealedKeyPublicKey(store.latest.publicKey);
|
|
105
136
|
const sealing = createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey);
|
|
106
137
|
const probeSession = "sealed-key-self-test";
|
|
107
138
|
let roundTrip;
|
|
@@ -144,6 +175,18 @@ function createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey) {
|
|
|
144
175
|
}
|
|
145
176
|
export async function reportExecutionPublicKey(opts) {
|
|
146
177
|
const { baseUrl, adminToken, key, notes, logger } = opts;
|
|
178
|
+
await _sodium.ready;
|
|
179
|
+
try {
|
|
180
|
+
judgeSealedKeyPublicKey(key.publicKey);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger?.warn("sealed_key_public_key_refused", {
|
|
184
|
+
publicKeyId: key.publicKeyId,
|
|
185
|
+
...(err instanceof SealedKeyPublicKeyRefusedError ? { flaw: err.flaw } : { errKind: err instanceof Error ? err.name : "unknown" }),
|
|
186
|
+
note: "this public key is not registered with the center and not printed for manual registration — fix or remove the key file in the sealed-key directory and restart",
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
147
190
|
const manual = (why) => {
|
|
148
191
|
logger?.warn("sealed_key_public_key", {
|
|
149
192
|
publicKeyId: key.publicKeyId,
|
package/dist/security.d.ts
CHANGED
|
@@ -316,6 +316,13 @@ export declare function createAuthorizer(config: ServiceConfig, sessionStore: Ow
|
|
|
316
316
|
/** F1/F3 (codex scope-ledger audit 2026-07-14) — the auth-boundary principal SHAPE gate, applied to every
|
|
317
317
|
* verified identity source (trusted header, SSO-bridge sub, direct-door JWT sub) so no downstream consumer
|
|
318
318
|
* ever sees an ambiguous or over-wide principal:
|
|
319
|
+
* - a string that is NOT A PRINCIPAL NAME is REJECTED (7.137.0): the engine's one judge `principalNameOf` (core 7.56.0 [ref] —
|
|
320
|
+
* visible content, no control / line-or-paragraph separator / format character other than the two joiners; kept verbatim, never
|
|
321
|
+
* normalized). The session stores' first ownership write and fork and the durable rule buckets read the same judge
|
|
322
|
+
* (`plugins/session-ownership.ts`, `permission-rule-store-sql.ts`); before this arm the boundary admitted what the store then refused,
|
|
323
|
+
* and the request fell into the generic 500 with its code lost. Typical source: a proxy writing raw UTF-8 bytes into the principal
|
|
324
|
+
* header — Node reads header bytes one per character (latin1), and a CJK character's continuation bytes often land on 0x80–0x9F,
|
|
325
|
+
* the C1 controls;
|
|
319
326
|
* - RESERVED internal identifiers are REJECTED (修1, 三路复审 A 组 HIGH — see {@link RESERVED_PRINCIPALS} /
|
|
320
327
|
* {@link RESERVED_PRINCIPAL_PREFIXES} below): a REAL tenant whose principal string-equals an internal scope
|
|
321
328
|
* sentinel would pass the plain string-compare owner/scope gates downstream and merge with internal rows —
|
|
@@ -328,8 +335,8 @@ export declare function createAuthorizer(config: ServiceConfig, sessionStore: Ow
|
|
|
328
335
|
* - length > 190 is REJECTED: every scope/owner column the principal lands in VERBATIM is VARCHAR(190)
|
|
329
336
|
* (sendfile_link.scope, approval.owner/scope, memory-engine scope, rate-limit keys, …) — an over-long
|
|
330
337
|
* principal would truncate/500 at the INSERT and split one identity into two.
|
|
331
|
-
* Throws a typed 400 `HttpError` (the HTTP layer maps it)
|
|
332
|
-
* the length cap), never a secret and never the internal scope's semantics. */
|
|
338
|
+
* Throws a typed 400 `HttpError` (the HTTP layer maps it): `principal_not_a_name` / `principal_reserved` / `principal_too_long`; the message
|
|
339
|
+
* names the rule (not a name / a reserved identifier / the length cap), never a secret and never the internal scope's semantics. */
|
|
333
340
|
export declare const PRINCIPAL_MAX_LENGTH = 190;
|
|
334
341
|
/**
|
|
335
342
|
* 一个被受理的主体串最多占多少个 UTF-8 字节 —— 按**字节**计宽的身份列(TiDB `VARBINARY(n)`)的列宽**唯一来源**(派生,不手抄第二个数)。
|
|
@@ -354,8 +361,13 @@ export declare const RESERVED_PRINCIPAL_PREFIXES: readonly string[];
|
|
|
354
361
|
/** The principal-shape rule as a VERDICT (the one judge; {@link assertPrincipalShape} is its throwing reader). `undefined` = a
|
|
355
362
|
* principal may carry this string; otherwise which rule it breaks. A reader that only needs "can this string be anyone?" (the
|
|
356
363
|
* organization delivery gate's recipient: a string that breaks the rule is nobody, refused with the same bytes as an unknown
|
|
357
|
-
* principal) reads the verdict instead of catching the throw.
|
|
358
|
-
|
|
364
|
+
* principal) reads the verdict instead of catching the throw.
|
|
365
|
+
*
|
|
366
|
+
* Order: "is it a name at all" first — the other two rules presuppose a name (a reserved identifier / a name too wide for the
|
|
367
|
+
* column), and the `reserved` sentence echoes the value while the `not_a_name` sentence never does (a string that names nobody may be a
|
|
368
|
+
* pasted credential). An empty string is `not_a_name` here (the engine's judge says so); a header reader that treats a blank header as
|
|
369
|
+
* "no principal" decides that before asking. */
|
|
370
|
+
export type PrincipalShapeProblem = "not_a_name" | "reserved" | "too_long";
|
|
359
371
|
export declare function principalShapeProblem(principal: string): PrincipalShapeProblem | undefined;
|
|
360
372
|
export declare function assertPrincipalShape(principal: string): void;
|
|
361
373
|
/** The checkpoint `scope` column's single-user/anonymous sentinel — the same "_" documented above
|
package/dist/security.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { uuidv7, PROJECT_ID_REGEX } from "@sema-agent/core";
|
|
1
|
+
import { uuidv7, PROJECT_ID_REGEX, principalNameOf } from "@sema-agent/core";
|
|
2
2
|
import { DEPLOYMENT_SHAPE_PREDICATES } from "./deployment-shape.js";
|
|
3
3
|
import { headReadOf } from "./session-incarnation.js";
|
|
4
4
|
import { memoryScopeFor } from "./memory-scope.js";
|
|
5
5
|
import { verifyPrincipalJwt } from "./principal-jwt.js";
|
|
6
6
|
import { fieldConflictFacts } from "./field-conflict.js";
|
|
7
7
|
import { ORPHAN_SESSION_OWNER_PREFIX } from "./delegated-session-owner.js";
|
|
8
|
+
import { NOT_A_PRINCIPAL_NAME } from "./plugins/session-ownership.js";
|
|
8
9
|
export function isDestructiveSessionWrite(method, url) {
|
|
9
10
|
if (method === "DELETE")
|
|
10
11
|
return /^\/v1\/sessions\/[^/]+$/.test(url);
|
|
@@ -134,6 +135,8 @@ export const PRINCIPAL_MAX_UTF8_BYTES = PRINCIPAL_MAX_LENGTH * 3;
|
|
|
134
135
|
export const RESERVED_PRINCIPALS = ["_", "__none__", "_leader"];
|
|
135
136
|
export const RESERVED_PRINCIPAL_PREFIXES = ["_leader-", ORPHAN_SESSION_OWNER_PREFIX];
|
|
136
137
|
export function principalShapeProblem(principal) {
|
|
138
|
+
if (principalNameOf(principal) === undefined)
|
|
139
|
+
return "not_a_name";
|
|
137
140
|
if (RESERVED_PRINCIPALS.includes(principal) || RESERVED_PRINCIPAL_PREFIXES.some((p) => principal.startsWith(p)))
|
|
138
141
|
return "reserved";
|
|
139
142
|
if (principal.length > PRINCIPAL_MAX_LENGTH)
|
|
@@ -145,10 +148,12 @@ export function assertPrincipalShape(principal) {
|
|
|
145
148
|
switch (problem) {
|
|
146
149
|
case undefined:
|
|
147
150
|
return;
|
|
151
|
+
case "not_a_name":
|
|
152
|
+
throw createCodedHttpError(400, "principal_not_a_name", `principal is not a principal name: got ${NOT_A_PRINCIPAL_NAME} — send the identity as visible characters only (a proxy that writes raw UTF-8 bytes into the principal header is read one byte per character, which can produce control characters: encode it upstream as ASCII or another form without them)`);
|
|
148
153
|
case "reserved":
|
|
149
|
-
throw
|
|
154
|
+
throw createCodedHttpError(400, "principal_reserved", `principal ${JSON.stringify(principal)} is reserved (an internal scope sentinel) — authenticate with a real tenant identifier`);
|
|
150
155
|
case "too_long":
|
|
151
|
-
throw
|
|
156
|
+
throw createCodedHttpError(400, "principal_too_long", `principal exceeds ${PRINCIPAL_MAX_LENGTH} characters (the scope-column width every store pins, e.g. sendfile_link.scope VARCHAR(190)); got ${principal.length}`);
|
|
152
157
|
default: {
|
|
153
158
|
const unknownProblem = problem;
|
|
154
159
|
throw new Error(`assertPrincipalShape: unknown problem ${String(unknownProblem)}`);
|
|
@@ -124,6 +124,12 @@ export declare function hostSubprocessEnv(o: HostSubprocessEnvRequest): NodeJS.P
|
|
|
124
124
|
* 既不另写一份组合(第二份会漂),也不把一次没发生的 spawn 记进剥除计数。`findings` 是组合点自己的收集器(引擎把形状臂与自有凭据臂的每一条扣下都推进去)。
|
|
125
125
|
*/
|
|
126
126
|
export declare function composeHostSubprocessEnv(o: Omit<HostSubprocessEnvRequest, "site">, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
|
127
|
+
/**
|
|
128
|
+
* core 7.56.0 [ref] `ExecutionEnv.withheldEnv` 的**宿主子进程**读法:{@link composeHostSubprocessEnv} 那只组合扣下的发现(名 + 规则,从不带值),
|
|
129
|
+
* 减去适配器显式授予重供的键(`regranted` 的自有键:子进程收得到那个名,不是损失)—— 引擎契约的原句「the findings of that composition less what its
|
|
130
|
+
* explicit grants re-supply」。同一个名字被两条规则各扣一次 ⇒ 按「名 + 规则」各记一笔(与披露同一去重)。只读组合:不披露、不计数(没有 spawn 发生)。
|
|
131
|
+
*/
|
|
132
|
+
export declare function withheldFromHostSubprocess(o: Omit<HostSubprocessEnvRequest, "site">, regranted: Readonly<Record<string, string>> | undefined): SecretEnvFinding[];
|
|
127
133
|
/** git 子进程腿的现场(闭集里名字带 `.git-` 的那几只 —— 编译期筛;它们都没有点名放行面)。 */
|
|
128
134
|
export type GitSubprocessSite = Extract<SecretEnvScrubSite, `${string}.git-${string}`>;
|
|
129
135
|
/**
|
|
@@ -86,6 +86,20 @@ export function composeHostSubprocessEnv(o, findings = []) {
|
|
|
86
86
|
const putBack = o.allow.length > 0 ? inheritedHostEnv([...o.allow], findings) : {};
|
|
87
87
|
return { ...inheritedHostEnv(o.policy ?? "scrub", findings), ...putBack };
|
|
88
88
|
}
|
|
89
|
+
export function withheldFromHostSubprocess(o, regranted) {
|
|
90
|
+
const findings = [];
|
|
91
|
+
composeHostSubprocessEnv(o, findings);
|
|
92
|
+
const withheld = [];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const f of findings) {
|
|
95
|
+
const k = `${f.key}=${f.kind}`;
|
|
96
|
+
if (seen.has(k) || (regranted !== undefined && Object.hasOwn(regranted, f.key)))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(k);
|
|
99
|
+
withheld.push(f);
|
|
100
|
+
}
|
|
101
|
+
return withheld;
|
|
102
|
+
}
|
|
89
103
|
export function gitSubprocessEnv(o) {
|
|
90
104
|
return {
|
|
91
105
|
...hostSubprocessEnv({ site: o.site, allow: [] }),
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ## 为什么多一个 `face` 参数(而不是单参)
|
|
12
12
|
* core 的 `invalid_session` 是**同词两义**(亲读 dist:`engine/session/import-validate.js` 的导入门拒调用方交来的
|
|
13
|
-
* 日志,`engine/session/storage-base.js` 的建树 /
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* ⇒ 面是映射的一维,写进属主,不散在消费点。
|
|
13
|
+
* 日志,`engine/session/storage-base.js` / `parent-chain-guard.js` 的建树 / 走根拒**已存**日志的结构 —— 叶子或父不在日志里、
|
|
14
|
+
* 条目是自己的祖先〔自指 / 成环〕)。前者是调用方的日志没过校验(import 面 422),后者是这只会话**存着的**日志坏了(open 面 500,
|
|
15
|
+
* 调用方没做错任何事)。单参签名装不下这件事:要么打开面的每个消费点都写一句 `code === "invalid_session"` 特判,要么把已存日志坏了
|
|
16
|
+
* 说成「你交的日志坏了」。⇒ 面是映射的一维,写进属主,不散在消费点。
|
|
18
17
|
* · `"open"` —— 为某只会话**打开它已存的日志**(提交各腿的引擎 prepare、鉴权门的归属探针、直接读会话的口);
|
|
19
18
|
* · `"import"` —— 校验 / 写入**调用方交来的**日志(2c session-sync 的暂存导入)。
|
|
20
19
|
*
|
|
@@ -24,7 +23,16 @@
|
|
|
24
23
|
* 原样上 wire,契约既有立场:不为上游码铸孪生码)。三种会话后端同词:文件店由 core 铸,两只 SQL 店由
|
|
25
24
|
* `plugins/session-event-payload.ts` 铸(坏的 `session_event.payload` 行)。
|
|
26
25
|
* · `invalid_session` —— `import` 面 **422 `invalid_entries`**(修前 session-sync 的那一行,码位 / 状态逐字不变);
|
|
27
|
-
* `open`
|
|
26
|
+
* `open` 面 **500 `invalid_session`**:这只会话已存的日志结构坏了(悬空父 / 自指 / 成环 / 叶子缺席),打得开存档、走不通分支。
|
|
27
|
+
* 句子发 core 的原句(点名条目 id,不含盘上路径),过既有脱敏。
|
|
28
|
+
* · **为什么是 500、不是 4xx / 503**:坏的是存着的数据,调用方没做错任何事;重试改不了判(修复在运维手里)—— 与 `corrupt_session` 同一档同一理由。
|
|
29
|
+
* · **为什么码是 `invalid_session`、不是 `corrupt_session`**:码不另铸、也不把一个 core 词改名成另一个 core 词。两个词说的是两件事
|
|
30
|
+
* (`corrupt_session` = 存档读不出;`invalid_session` = 读得出、结构走不通),而 run 终局上它本来就是引擎词(`terminal.code`,同步口 4xx / 5xx 体的
|
|
31
|
+
* `errorCode` 取终局码原样):读口答 `corrupt_session`、提交口答 `invalid_session` 就是同一件事两个名字。所以映射只有一格,
|
|
32
|
+
* 读口与提交口的 run 终局分诊({@link sessionErrorHttp} 的 open 面,`resumeAtHttpStatus` 读它)同一只、同码同状态。
|
|
33
|
+
* · **没有 `reason` 机读位**:core 的 `SessionError` 只带 `code` 与句子;悬空父与叶子缺席是同一句 `Entry <id> not found`,自指与成环是同一句
|
|
34
|
+
* `is its own ancestor` —— 本仓分出 {parent_missing, parent_self, parent_cycle} 只有解析句子或再写一只判官两条路(会话视图自己的走链器已在 core 7.54.0 提货时退役:
|
|
35
|
+
* 视图读的就是引擎读的那条分支,见 `plugins/local-session-window.ts` 头注)。要分,请求 core 把种类放进错误(同 `placement_kind` 的先例),不在这里猜。
|
|
28
36
|
* · `not_found` —— 留白:每一面各有自己的「不存在」答法(404 `not_found.session` / 同步口的 `resume.session_not_found` /
|
|
29
37
|
* 列表里的缺席),码位各不相同,不由本表统一。
|
|
30
38
|
* · `placement_refused` —— `import` 面 **409 `conflict.session_placed`**(机读位 `detail.placement_kind`,固定句);`open` 面留白。
|
|
@@ -3,6 +3,7 @@ export const PLACED_SESSION_SENTENCE = "this session holds a delegated agent's t
|
|
|
3
3
|
const PLACEMENT_KIND = "subagent";
|
|
4
4
|
export const STAGED_IMPORT_OWNER_CHANGED_SENTENCE = "session owner changed while the import streamed; nothing was imported — start the sync again";
|
|
5
5
|
const CORRUPT_SESSION = { status: 500, errorCode: "corrupt_session" };
|
|
6
|
+
const STORED_LOG_INVALID = { status: 500, errorCode: "invalid_session" };
|
|
6
7
|
const INVALID_ENTRIES = { status: 422, errorCode: "invalid_entries", sentence: STAGED_IMPORT_INVALID_SENTENCE };
|
|
7
8
|
const SESSION_PLACED = { status: 409, errorCode: "conflict.session_placed", sentence: PLACED_SESSION_SENTENCE, detail: { placement_kind: PLACEMENT_KIND } };
|
|
8
9
|
const STAGED_IMPORT_OWNER_CHANGED = { status: 409, errorCode: "conflict", sentence: STAGED_IMPORT_OWNER_CHANGED_SENTENCE };
|
|
@@ -11,7 +12,7 @@ export function sessionErrorHttp(code, face) {
|
|
|
11
12
|
case "corrupt_session":
|
|
12
13
|
return CORRUPT_SESSION;
|
|
13
14
|
case "invalid_session":
|
|
14
|
-
return face === "import" ? INVALID_ENTRIES :
|
|
15
|
+
return face === "import" ? INVALID_ENTRIES : STORED_LOG_INVALID;
|
|
15
16
|
case "placement_refused":
|
|
16
17
|
return face === "import" ? SESSION_PLACED : undefined;
|
|
17
18
|
case "conflict":
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* · **一位活住客必须有令牌 —— 读到空化身 ⇒ 响亮拒**({@link SessionIncarnationMissingError}):本仓比 core 多的那一条律,只住在一处 ——
|
|
28
28
|
* 把头投成 {@link SessionFace}(属主 + 一只令牌)的 {@link sessionFaceOfHead}(批量来源读与换装回读用同一只 {@link sessionFaceFromRow})。
|
|
29
29
|
* 要拿令牌去**写**(带化身条件的 UPDATE、认领、空间分区键)或要响亮区分「行坏了」的读者经它;只**比较**两头的读者用 `compareOccupants`。
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* 空值只可能来自没铸化身的写腿、人为改库,或跳过了 7.104.0–7.135.x(那几版的起服迁移给存量行铸令牌,窗关后已删码)直接升上来的库 ——
|
|
31
|
+
* 把它当「通配」就是让任何人都能和这一行比成同一位住客。
|
|
32
32
|
* · **不上 wire、不进模型上下文、不进日志正文**:日志里要点名时只打 {@link incarnationLogPrefix}(前 8 位)。
|
|
33
33
|
*
|
|
34
34
|
* ## 谁读它(全部派生自那一只头读)
|
|
@@ -97,8 +97,19 @@ export declare class UnreadableLeaf {
|
|
|
97
97
|
* 与 core 的读法同义 —— 拒不拒是投影的事,见 {@link sessionFaceOfHead})。叶子那一格读不出({@link UnreadableLeaf})⇒ 头照答属主与住客,
|
|
98
98
|
* 读 `leafId` 的那一位收到那只故障 —— 永不把读不出的叶子答成 `null`(`null` = 空会话)。 */
|
|
99
99
|
export declare function headFromRow(sessionId: string, owner: string | null, incarnation: unknown, leaf: string | null | UnreadableLeaf): SessionHead;
|
|
100
|
+
/**
|
|
101
|
+
* 🔴 **会话内容读不读得出 —— 会话路由在门放行之后判它的唯一读法**:读头的叶子。内容读不出的头({@link UnreadableLeaf})在这一读上抛那只故障本身
|
|
102
|
+
* (码 `corrupt_session`,经 `sessionErrorHttp` 的 open 面落 500);读得出 ⇒ 叶子 id(`null` = 空会话)。
|
|
103
|
+
* 谁在**门放行之后**要动这只会话的内容(读叶子本身、开事件流、改名 —— 改名答的是列表行,列表读存档),谁就经它读一次;
|
|
104
|
+
* 门之前不许读(非属主对坏档与对好档同答,见头注「头答自会话行」)。三个口各写一句 `head.leafId` 与各写一句"存档坏了怎么办"是同一件事的三种答法
|
|
105
|
+
* (修前:头探针 500、事件流 200 零帧 —— 观察探针把每拍读叶子抛出的故障当瞬时错吞掉 —— 改名 404 —— 列表跳过读不出的存档,会话被说成不存在)。
|
|
106
|
+
* SQL 两店的叶子是行上的一列,永不抛 ⇒ 这一读在 SQL 上恒等于 `head.leafId`,这条判据只对内容与行分居两处的店(local)有牙。
|
|
107
|
+
* 事件流只在开流之前判一次;流开了之后存档才坏,观察探针仍把读叶子的故障当瞬时错续探(流不断、不再产出新帧)。
|
|
108
|
+
*/
|
|
109
|
+
export declare function leafOfAdmittedHead(head: SessionHead): string | null;
|
|
100
110
|
/** 一只头 → 一位活住客({@link SessionFace}):`undefined` 头 = 没有这只会话 ⇒ `undefined`;行在而没有令牌 ⇒ {@link SessionIncarnationMissingError}
|
|
101
111
|
* (本仓的那条律只住在这里 + {@link sessionFaceFromRow})。 */
|
|
112
|
+
export declare function sessionFaceOfHead(sessionId: string, head: SessionHead): SessionFace;
|
|
102
113
|
export declare function sessionFaceOfHead(sessionId: string, head: SessionHead | undefined): SessionFace | undefined;
|
|
103
114
|
/**
|
|
104
115
|
* 「这只会话店有没有按 id 读(`headOf`)」—— 全仓**只判这一处**:授权口、各路由的属主门、能力位、起服门、k8s 工作区空间的每 run 读与
|
|
@@ -116,12 +127,12 @@ export declare function carriedIncarnation(sessionId: string, prior: {
|
|
|
116
127
|
} | undefined, owner: string | null): string;
|
|
117
128
|
/**
|
|
118
129
|
* `session_meta.incarnation` 的列定义 —— 两方言各一句,**唯一属主**:中央 DDL(`tidb-pool.ts` / `pg-session-storage.ts` 的
|
|
119
|
-
* `CREATE TABLE session_meta`)
|
|
120
|
-
*
|
|
130
|
+
* `CREATE TABLE session_meta`)内插这同一串;存量库缺这一列时,起服列册探针(`schema-consistency.ts`)拒启,拒句里的 `ALTER TABLE … ADD COLUMN …`
|
|
131
|
+
* 由同一份建表语句解析生成 ⇒ 「新库建出来的列」与「运维按拒句补上的列」逐字同形(探针比的是列名,形由这里保证)。
|
|
121
132
|
*
|
|
122
133
|
* `NOT NULL DEFAULT ''`:空串就是「没铸过」的唯一表示 —— 读口对它响亮拒({@link SessionIncarnationMissingError})。
|
|
123
134
|
* 缺省值只为两件事存在:①存量表 `ADD COLUMN` 在两方言上一句成立(PG 对非空表加无缺省的 NOT NULL 列直接拒);
|
|
124
|
-
* ②一条漏铸的写腿落下的是**读得出来的错**(读口拒),而不是一条插入期的 500 —— 两形都 fail-closed
|
|
135
|
+
* ②一条漏铸的写腿落下的是**读得出来的错**(读口拒),而不是一条插入期的 500 —— 两形都 fail-closed。
|
|
125
136
|
* 宽度 64:`randomUUID()` 是 36 字符,留出余量但不给任意长串留位。
|
|
126
137
|
*/
|
|
127
138
|
export declare const SESSION_META_INCARNATION_COLUMN: {
|
|
@@ -13,10 +13,10 @@ export class SessionIncarnationMissingError extends Error {
|
|
|
13
13
|
sessionId;
|
|
14
14
|
code = "session.incarnation_missing";
|
|
15
15
|
constructor(sessionId) {
|
|
16
|
-
super(`session ${sessionId} has no incarnation token: every session row is minted one when it is created
|
|
17
|
-
`
|
|
18
|
-
`
|
|
19
|
-
`
|
|
16
|
+
super(`session ${sessionId} has no incarnation token: every session row is minted one when it is created. An empty value means a write path ` +
|
|
17
|
+
`that did not mint one, a hand-edited row, or a database upgraded from before 7.104.0 straight to a version after 7.135.x (a boot of any ` +
|
|
18
|
+
`version in between minted tokens for the older rows; the code that did so has since been removed) — it is refused rather than read as ` +
|
|
19
|
+
`"matches anyone". Give the row a distinct random token (any opaque string of at most 64 characters), or delete the session.`);
|
|
20
20
|
this.sessionId = sessionId;
|
|
21
21
|
this.name = "SessionIncarnationMissingError";
|
|
22
22
|
}
|
|
@@ -57,6 +57,9 @@ export function headFromRow(sessionId, owner, incarnation, leaf) {
|
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
export function leafOfAdmittedHead(head) {
|
|
61
|
+
return head.leafId;
|
|
62
|
+
}
|
|
60
63
|
export function sessionFaceOfHead(sessionId, head) {
|
|
61
64
|
return head === undefined ? undefined : sessionFaceFromRow(sessionId, head.owner, head.incarnation);
|
|
62
65
|
}
|
|
@@ -52,7 +52,7 @@ export declare function namespaceCoveringForms(): readonly {
|
|
|
52
52
|
}[];
|
|
53
53
|
/**
|
|
54
54
|
* 能力位 `sessionRulesNamespaceForms` 的**唯一**判据:会话规则记录里协议命名空间的覆盖形,引擎认成「一组工具」**且**写口收。
|
|
55
|
-
* 对 `PROTOCOL_TABLE` 的每一员:① 它的三只覆盖形,引擎的匹配器(`
|
|
55
|
+
* 对 `PROTOCOL_TABLE` 的每一员:① 它的三只覆盖形,引擎的匹配器(`ruleNameCovers`,会话层与其余规则面同一只)都判得出覆盖那一员的真工具名
|
|
56
56
|
* (只当精确名收的写口不算 ——「收」与「认成一组」是两问);② 三形放进 `toolAllow` 与 `toolDeny` 两桶,两条写入腿共用的写口读者
|
|
57
57
|
* {@link readWritableSessionPermissionRules}(= 引擎写口判官)不拒。表非空 ∧ 每员都过 ⇒ `true`。
|
|
58
58
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROTOCOL_TABLE, SessionPolicyError, judgeSessionRulesWrite,
|
|
1
|
+
import { PROTOCOL_TABLE, SessionPolicyError, judgeSessionRulesWrite, ruleNameCovers } from "@sema-agent/core";
|
|
2
2
|
import { displayListEntry } from "./list-entry-display.js";
|
|
3
3
|
function isSessionRulesRefusal(e) {
|
|
4
4
|
return e instanceof SessionPolicyError && (e.code === "invalid_shape" || e.code === "inert_entry");
|
|
@@ -25,7 +25,7 @@ export function namespaceCoveringForms() {
|
|
|
25
25
|
export function sessionRulesNamespaceFormsHonored() {
|
|
26
26
|
const probes = namespaceCoveringForms();
|
|
27
27
|
return (probes.length > 0 &&
|
|
28
|
-
probes.every(({ forms, tool }) => forms.every((form) =>
|
|
28
|
+
probes.every(({ forms, tool }) => forms.every((form) => ruleNameCovers(form, tool)) && readWritableSessionPermissionRules({ toolAllow: [...forms], toolDeny: [...forms] }).refusal === undefined));
|
|
29
29
|
}
|
|
30
30
|
export function readSessionRulesRecords(raw) {
|
|
31
31
|
if (!Array.isArray(raw))
|
package/dist/session-row.d.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话行的**可写位**(`title` / `pinned`)—— `PATCH /v1/sessions/:id` 的体闭集、两键的判据,与 2c 导入同读的那一套(唯一属主)。
|
|
3
|
-
*
|
|
4
|
-
* ## 一列一事实,写者次序成文
|
|
5
|
-
* · `title` 是一列(SQL `session_meta.title` / local 标题边档),两个写者,一条律:**用户**经会话店唯一写口 `patchSessionMeta`
|
|
6
|
-
* **无条件**写 —— 写一个串 = 改名,写 `null` = **清回自动标题**(列回到 NULL);**自动标题器**经 `setTitleIfNull` **只填 NULL**
|
|
7
|
-
* (每次提交按列读一次:列是 NULL 才起名,见 `session-titler.ts`)。所以清与填是同一列同一律:用户清掉之后,下一次提交由自动标题器
|
|
8
|
-
* 照常填;用户写过串的会话,自动标题器永不覆盖(无条件写盖得掉自动标题,NULL 守卫盖不掉用户标题)。没有第二列(不另设 `userTitle` /
|
|
9
|
-
* `label`)。清的拼法只有 `null` 一种 —— 空串 `""` 是坏体(`request.body_shape`),不是「清」。
|
|
10
|
-
* · `pinned` 是一列布尔,缺省 `false`;列表行**恒在场**(置顶是会话的状态,不是能力)。
|
|
11
|
-
*
|
|
12
|
-
* ## 上界 = 列宽
|
|
13
|
-
* {@link SESSION_TITLE_MAX_CHARS} 是 `title` 列的宽度,两方言 DDL 内插的就是这一个数(`tidb-pool.ts` / `pg-session-storage.ts`),
|
|
14
|
-
* wire 上界与存储上界是同一个事实。按**字符**计(Unicode 码点 —— 与 MySQL 协议 utf8mb4 / PostgreSQL 的 `VARCHAR(n)` 同一单位),
|
|
15
|
-
* 不按 UTF-16 位计。自动标题器自己的显示预算(`session-titler.ts` 的 `TITLE_MAX_CHARS`)更短,落在这个上界之内。
|
|
16
|
-
*/
|
|
17
1
|
/** `title` 列的宽度(字符)= 用户标题的上界。两方言 DDL 内插它。 */
|
|
18
2
|
export declare const SESSION_TITLE_MAX_CHARS = 120;
|
|
19
3
|
/** 会话行的可写位(体闭集;两键至少一键)。会话店唯一写口 `patchSessionMeta` 的形参。`title: null` = 清回自动标题(列写 NULL)。 */
|
|
@@ -28,7 +12,9 @@ export interface SessionRowProblem {
|
|
|
28
12
|
}
|
|
29
13
|
/**
|
|
30
14
|
* `title` 一格的**唯一**读者(PATCH 体与 2c 导入同读)。`null` ⇒ 清回自动标题;串 ⇒ 1..{@link SESSION_TITLE_MAX_CHARS} 个字符、首尾无空白、
|
|
31
|
-
* 不含 C0 控制字符({@link C0_CONTROL})
|
|
15
|
+
* 不含 C0 控制字符({@link C0_CONTROL})、至少有一个可见字符(经 core 的 `normalizePeerName` 不为空 —— NFKC → 去控制 / 格式码点 → trim;
|
|
16
|
+
* 与跨会话对端目录判「这一行有没有可用名」是**同一只**判据,`peer-directory.ts`),存原文(不脱敏、不改写);空串 ⇒ `request.body_shape`(清的拼法只有 `null`);其余 ⇒ `request.field_invalid`。
|
|
17
|
+
* 三道串判的先后固定:边空白 → C0 → 无可见字符(带 C0 的串先报 C0)。
|
|
32
18
|
*/
|
|
33
19
|
export declare function readSessionTitle(v: unknown): {
|
|
34
20
|
readonly title: string | null;
|
package/dist/session-row.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { normalizePeerName } from "@sema-agent/core";
|
|
1
2
|
export const SESSION_TITLE_MAX_CHARS = 120;
|
|
2
3
|
const SESSION_ROW_PATCH_KEYS = { title: true, pinned: true };
|
|
3
4
|
const BODY_SHAPE_MESSAGE = "the body must be a JSON object carrying at least one of `title` and `pinned`, and nothing else";
|
|
4
5
|
const EMPTY_TITLE_MESSAGE = "title must not be the empty string — send `null` to clear it back to the automatic title";
|
|
6
|
+
const INVISIBLE_TITLE_MESSAGE = "title must contain at least one visible character";
|
|
5
7
|
const C0_CONTROL = /[\u0000-\u001f]/u;
|
|
6
8
|
export function readSessionTitle(v) {
|
|
7
9
|
if (v === null)
|
|
@@ -16,6 +18,8 @@ export function readSessionTitle(v) {
|
|
|
16
18
|
return { problem: { code: "request.field_invalid", message: "title must not start or end with whitespace" } };
|
|
17
19
|
if (C0_CONTROL.test(v))
|
|
18
20
|
return { problem: { code: "request.field_invalid", message: "title must not contain control characters (U+0000 to U+001F)" } };
|
|
21
|
+
if (normalizePeerName(v) === "")
|
|
22
|
+
return { problem: { code: "request.field_invalid", message: INVISIBLE_TITLE_MESSAGE } };
|
|
19
23
|
return { title: v };
|
|
20
24
|
}
|
|
21
25
|
export function readSessionPinned(v) {
|