@bivy/bivy 0.10.1-staging.291 → 0.10.1-staging.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +66 -1
- package/dist/session/session-contract.js +163 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -100,6 +100,7 @@ import { AttachmentStore, isValidAttachmentHash } from "./session/attachment-sto
|
|
|
100
100
|
import { planAttachment, isAttachPlanError, MAX_AGENT_ATTACHMENT_BYTES } from "./session/attach-to-chat.js";
|
|
101
101
|
import { ReplicationService } from "./session/replication-service.js";
|
|
102
102
|
import { createSessionNewDedupe } from "./session/session-new-dedupe.js";
|
|
103
|
+
import { computeSessionContract } from "./session/session-contract.js";
|
|
103
104
|
import { SecretVault, resolveSecret } from "./secrets.js";
|
|
104
105
|
import { deviceFlowClientId, requestDeviceCode, pollAccessTokenOnce, REPO_CONNECT_SCOPE } from "./github-device-auth.js";
|
|
105
106
|
import { InstallationTokenCache, createAppJwt, resolveInstallationId } from "./github-app-auth.js";
|
|
@@ -2120,6 +2121,7 @@ const RELAY_COMMANDS = {
|
|
|
2120
2121
|
approvalMode: rec?.approvalMode,
|
|
2121
2122
|
ephemeral: rec?.ephemeral,
|
|
2122
2123
|
executionProfile: rec ? (rec.ephemeral ? "isolated_customer_cloud" : "trusted_workstation") : undefined,
|
|
2124
|
+
contract: rec?.contract ?? meta?.contract,
|
|
2123
2125
|
auditHealth: rec ? auditLog.health() : undefined,
|
|
2124
2126
|
eventLogHealth: rec ? eventLogHealthForSession(rec.id) : undefined,
|
|
2125
2127
|
prUrl: rec?.prUrl ?? meta?.prUrl,
|
|
@@ -2996,6 +2998,29 @@ const RELAY_COMMANDS = {
|
|
|
2996
2998
|
const workspaceInput = typeof msg.workspace === "string" ? msg.workspace.trim() : "";
|
|
2997
2999
|
const title = typeof msg.title === "string" ? msg.title : undefined;
|
|
2998
3000
|
const requestId = typeof msg.requestId === "string" ? msg.requestId : undefined;
|
|
3001
|
+
// Prevent silent downgrade: for a "supported" (certified) agent whose live
|
|
3002
|
+
// protection would be degraded, require the client to have explicitly
|
|
3003
|
+
// acknowledged it (the picker's confirm-to-continue step) before this
|
|
3004
|
+
// request is allowed to launch a session — checked before any side-effecting
|
|
3005
|
+
// work (repo clone / worktree allocation) so a rejection is cheap. Gating
|
|
3006
|
+
// facts (protectionLevel, executionMode, capabilities, supportTier) are all
|
|
3007
|
+
// runtime-level, not workspace-policy-dependent, so this pre-creation check
|
|
3008
|
+
// stays correct regardless of what per-workspace sandbox/approval policy
|
|
3009
|
+
// resolves to afterward.
|
|
3010
|
+
const acknowledgeReducedProtections = msg.acknowledgeReducedProtections === true;
|
|
3011
|
+
const gateNow = new Date().toISOString();
|
|
3012
|
+
const rt = getRuntime(agentFrom(msg) ?? defaultRuntimeId);
|
|
3013
|
+
const gateContract = computeSessionContract({ runtime: rt, preview: false, sandbox: sandboxFrom(msg), acknowledgedAt: acknowledgeReducedProtections ? gateNow : undefined }, gateNow);
|
|
3014
|
+
if (gateContract.requiresAcknowledgement) {
|
|
3015
|
+
relay?.sendEvent({
|
|
3016
|
+
type: "session.error",
|
|
3017
|
+
code: "reduced_protections_ack_required",
|
|
3018
|
+
error: `${rt.displayName || rt.id} would run this session with reduced protections for a certified profile. Confirm to continue.`,
|
|
3019
|
+
contract: gateContract,
|
|
3020
|
+
requestId,
|
|
3021
|
+
});
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
2999
3024
|
let record;
|
|
3000
3025
|
try {
|
|
3001
3026
|
// Deduped by requestId so a client's post-reconnect retry adopts the
|
|
@@ -3025,6 +3050,11 @@ const RELAY_COMMANDS = {
|
|
|
3025
3050
|
relay?.sendEvent({ type: "session.error", error: error instanceof Error ? error.message : String(error) });
|
|
3026
3051
|
return;
|
|
3027
3052
|
}
|
|
3053
|
+
// Resolve once from the session's actual, now-known launch facts (final
|
|
3054
|
+
// sandbox/approval after per-workspace policy) and store it — never
|
|
3055
|
+
// live-recomputed later, so it can't silently "improve" behind the user.
|
|
3056
|
+
record.contract = computeSessionContract({ runtime: getRuntime(record.runtimeId), preview: false, sandbox: record.sandbox, approvalMode: record.approvalMode, acknowledgedAt: acknowledgeReducedProtections ? gateNow : undefined }, gateNow);
|
|
3057
|
+
persistSessionMetadata(record);
|
|
3028
3058
|
relay?.sendEvent({
|
|
3029
3059
|
...transcripts.buildHistoryEvent({
|
|
3030
3060
|
sessionId: record.id,
|
|
@@ -6049,6 +6079,7 @@ function bivySessionEnvelope(record) {
|
|
|
6049
6079
|
approvalMode: record.approvalMode,
|
|
6050
6080
|
ephemeral: record.ephemeral,
|
|
6051
6081
|
executionProfile: record.ephemeral ? "isolated_customer_cloud" : "trusted_workstation",
|
|
6082
|
+
contract: record.contract,
|
|
6052
6083
|
auditHealth: auditLog.health(),
|
|
6053
6084
|
eventLogHealth: eventLogHealthForSession(record.id),
|
|
6054
6085
|
repoSlug: gh.repoSlug,
|
|
@@ -6094,6 +6125,7 @@ function persistSessionMetadata(record, status = sessionStatus(record)) {
|
|
|
6094
6125
|
runtimeId: record.runtimeId,
|
|
6095
6126
|
sandbox: record.sandbox,
|
|
6096
6127
|
agentName: getRuntime(record.runtimeId).displayName,
|
|
6128
|
+
contract: record.contract,
|
|
6097
6129
|
status,
|
|
6098
6130
|
branch: record.worktree?.branch,
|
|
6099
6131
|
worktree: record.worktree?.path,
|
|
@@ -6140,6 +6172,7 @@ function bivySessionEnvelopeFromSummary(s, rec, meta) {
|
|
|
6140
6172
|
// `rec` is undefined here (the live path returned above via bivySessionEnvelope),
|
|
6141
6173
|
// so the tier comes from the persisted metadata row.
|
|
6142
6174
|
sandbox: normalizeSandboxTier(meta?.sandbox),
|
|
6175
|
+
contract: meta?.contract,
|
|
6143
6176
|
repoSlug,
|
|
6144
6177
|
issueNumber: gh.issueNumber,
|
|
6145
6178
|
issueUrl,
|
|
@@ -7760,7 +7793,22 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
7760
7793
|
// bump it to now (only real activity should reorder the sidebar). A brand-new
|
|
7761
7794
|
// session legitimately starts "active now".
|
|
7762
7795
|
const resumedLastActive = requestedSessionFile ? metaLastActiveMs(storedMeta) : undefined;
|
|
7763
|
-
|
|
7796
|
+
// Rehydrate the persisted contract on resume/reopen (not recomputed — a
|
|
7797
|
+
// stored contract reflects what the original launch actually got, per
|
|
7798
|
+
// session-contract.ts). Brand-new sessions (no storedMeta) start undefined
|
|
7799
|
+
// here; session.new stamps a freshly computed one right after creation.
|
|
7800
|
+
// Rehydrating (rather than leaving this undefined for a resumed session)
|
|
7801
|
+
// also avoids a persistSessionMetadata call later silently clobbering the
|
|
7802
|
+
// stored contract with undefined via its `{...prev, ...input}` merge.
|
|
7803
|
+
const record = { id: sessionId, session, runtimeId: rt.id, sandbox: sessionSandbox, approvalMode: sessionSafety.approval, workspace: sessionWorkspace, sessionFile: session.sessionFile, agentServiceAddress: attachedAddress ?? rt.agentServiceAddress, worktree, source, prUrl: storedMeta?.prUrl, prs: storedMeta?.prs, contract: storedMeta?.contract, lastTouchedAt: resumedLastActive ?? Date.now(), warning: modelFallbackMessage, ephemeral: opts.ephemeral, workspaceState: runGit(["status", "--porcelain", "--untracked-files=normal"], sessionWorkspace) ? "dirty" : "clean" };
|
|
7804
|
+
// Migration: a session resumed/reopened from before this feature (or from a
|
|
7805
|
+
// node that predates it) has no stored contract. Stamp an honest one now
|
|
7806
|
+
// from currently-observed facts rather than leaving it blank forever or
|
|
7807
|
+
// inventing what launch actually got years ago — the same "don't fabricate
|
|
7808
|
+
// history" rule session-contract.ts applies elsewhere.
|
|
7809
|
+
if (!record.contract) {
|
|
7810
|
+
record.contract = computeSessionContract({ runtime: getRuntime(rt.id), preview: false, sandbox: sessionSandbox, approvalMode: sessionSafety.approval }, new Date().toISOString());
|
|
7811
|
+
}
|
|
7764
7812
|
// Apply this session's sandbox network policy as a per-session egress proxy
|
|
7765
7813
|
// (its own proxy/decider, never the node-global one). Opt-in via BIVY_SANDBOX_NET:
|
|
7766
7814
|
// a read-only session then actually blocks outbound network even for a CLI agent
|
|
@@ -9540,6 +9588,21 @@ app.post("/api/session", async (req, res, next) => {
|
|
|
9540
9588
|
if (!parsed)
|
|
9541
9589
|
return res.status(400).json({ error: `Invalid repository "${repoInput}" — use owner/repo.` });
|
|
9542
9590
|
}
|
|
9591
|
+
// Same downgrade gate as the relay session.new handler — this direct API
|
|
9592
|
+
// route (CLI/scripted callers, no picker in front of it) must not launch a
|
|
9593
|
+
// "supported" profile with degraded live protection any more silently than
|
|
9594
|
+
// the PWA can.
|
|
9595
|
+
const acknowledgeReducedProtections = req.body?.acknowledgeReducedProtections === true;
|
|
9596
|
+
const gateNow = new Date().toISOString();
|
|
9597
|
+
const rt = getRuntime(agentFrom(req.body ?? {}) ?? defaultRuntimeId);
|
|
9598
|
+
const gateContract = computeSessionContract({ runtime: rt, preview: false, sandbox: sandboxFrom(req.body ?? {}), acknowledgedAt: acknowledgeReducedProtections ? gateNow : undefined }, gateNow);
|
|
9599
|
+
if (gateContract.requiresAcknowledgement) {
|
|
9600
|
+
return res.status(409).json({
|
|
9601
|
+
error: `${rt.displayName || rt.id} would run this session with reduced protections for a certified profile. Pass acknowledgeReducedProtections: true to confirm.`,
|
|
9602
|
+
code: "reduced_protections_ack_required",
|
|
9603
|
+
contract: gateContract,
|
|
9604
|
+
});
|
|
9605
|
+
}
|
|
9543
9606
|
let session;
|
|
9544
9607
|
try {
|
|
9545
9608
|
// Deduped by requestId so a direct client's post-reconnect retry adopts the
|
|
@@ -9558,6 +9621,8 @@ app.post("/api/session", async (req, res, next) => {
|
|
|
9558
9621
|
return res.status(502).json({ error: `Could not clone ${parsed.slug}: ${error instanceof Error ? error.message : String(error)}` });
|
|
9559
9622
|
throw error;
|
|
9560
9623
|
}
|
|
9624
|
+
session.contract = computeSessionContract({ runtime: getRuntime(session.runtimeId), preview: false, sandbox: session.sandbox, approvalMode: session.approvalMode, acknowledgedAt: acknowledgeReducedProtections ? gateNow : undefined }, gateNow);
|
|
9625
|
+
persistSessionMetadata(session);
|
|
9561
9626
|
res.json({ id: session.id, workspace: session.workspace, source: session.source, branch: session.worktree?.branch, prUrl: session.prUrl, sessionFile: session.sessionFile, name: session.session.getName(), runtimeId: session.runtimeId, agentName: getRuntime(session.runtimeId).displayName, model: publicModel(session.session.getCurrentModel(), session.session.getCurrentModel()), sessionState: sessionState(session) });
|
|
9562
9627
|
}
|
|
9563
9628
|
catch (error) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// The Effective Session Contract — what a launched session actually got
|
|
5
|
+
// (detected agent/version, resolved execution mode, model/auth source,
|
|
6
|
+
// resume support, structured streaming, tool interception/approval
|
|
7
|
+
// semantics, sandbox enforcement) as distinct from the catalog-level
|
|
8
|
+
// `RuntimeInfo` promise. An observation report, not an attestation — same
|
|
9
|
+
// discipline as `src/audit/receipt-evidence.ts`: report only what was
|
|
10
|
+
// observed, keep "requested" and "effective" distinct, and turn every gap
|
|
11
|
+
// into a typed, honest reason instead of a silent absence or an invented
|
|
12
|
+
// default.
|
|
13
|
+
//
|
|
14
|
+
// `resolveSessionContract` here mirrors `packages/core/src/session-contract.ts`
|
|
15
|
+
// field-for-field. Not a shared import: the node (src/) intentionally does
|
|
16
|
+
// not depend on @bivy/core (a browser/client package — see
|
|
17
|
+
// src/session/inline-image-fetch.ts's comment for the same convention). Kept
|
|
18
|
+
// in lock-step by comment instead. If you change one, change the other.
|
|
19
|
+
export const SESSION_CONTRACT_SCHEMA_VERSION = "session-contract.v1";
|
|
20
|
+
const LIMITATIONS = {
|
|
21
|
+
agent_version_unknown: "The running agent's build/version was not reported; capability guarantees fall back to the pinned default.",
|
|
22
|
+
execution_mode_unstructured: "The agent is driven over a raw text pipe, not a structured message/event stream — tool-call and turn boundaries are inferred, not reported.",
|
|
23
|
+
execution_mode_unknown: "The session's communication mode has not been resolved yet.",
|
|
24
|
+
auth_unknown: "The credential kind backing this session was not identified.",
|
|
25
|
+
auth_unconfigured: "No credential is configured for this provider.",
|
|
26
|
+
resume_unsupported: "This agent cannot resume a persisted session; a stopped session restarts from a seeded continuation instead.",
|
|
27
|
+
tool_interception_unavailable: "Tool calls are not intercepted by Bivy's approval gate; the agent runs unmoderated.",
|
|
28
|
+
tool_interception_mcp_only: "Only this agent's MCP tool calls are gated by Bivy's approval flow — its built-in tools are not.",
|
|
29
|
+
sandbox_unavailable: "No sandbox or tool-control enforcement was observed; the agent runs with the OS user's full permissions.",
|
|
30
|
+
sandbox_observed_only: "Containment relies on Bivy's own tool controls rather than the agent's native sandbox — observed, not independently enforced.",
|
|
31
|
+
};
|
|
32
|
+
function degradedReason(area, code) {
|
|
33
|
+
return { area, code, message: LIMITATIONS[code] ?? code };
|
|
34
|
+
}
|
|
35
|
+
const PROTECTION_AREAS = new Set(["auth", "resume", "toolInterception", "sandbox"]);
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an Effective Session Contract from already-observed facts. Never
|
|
38
|
+
* infers a fact the caller didn't supply — an absent input becomes an honest
|
|
39
|
+
* "unknown"/"unavailable" state plus a typed reason, never a guess.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveSessionContract(input) {
|
|
42
|
+
const degradedReasons = [];
|
|
43
|
+
const versionSource = input.detectedVersion
|
|
44
|
+
? (input.versionSource ?? "reported")
|
|
45
|
+
: "unknown";
|
|
46
|
+
if (versionSource === "unknown")
|
|
47
|
+
degradedReasons.push(degradedReason("agent", "agent_version_unknown"));
|
|
48
|
+
const agent = {
|
|
49
|
+
id: input.agentId,
|
|
50
|
+
...(input.agentDisplayName ? { displayName: input.agentDisplayName } : {}),
|
|
51
|
+
...(input.detectedVersion ? { detectedVersion: input.detectedVersion } : {}),
|
|
52
|
+
versionSource,
|
|
53
|
+
};
|
|
54
|
+
const effectiveMode = input.executionMode ?? "unknown";
|
|
55
|
+
const structuredStreaming = effectiveMode === "protocol" || effectiveMode === "structured-pipe";
|
|
56
|
+
const executionModeState = effectiveMode === "unknown" ? "unavailable" : structuredStreaming ? "guaranteed" : "degraded";
|
|
57
|
+
if (executionModeState === "degraded")
|
|
58
|
+
degradedReasons.push(degradedReason("executionMode", "execution_mode_unstructured"));
|
|
59
|
+
if (executionModeState === "unavailable")
|
|
60
|
+
degradedReasons.push(degradedReason("executionMode", "execution_mode_unknown"));
|
|
61
|
+
const executionMode = { effective: effectiveMode, structuredStreaming, state: executionModeState };
|
|
62
|
+
const model = {
|
|
63
|
+
...(input.provider ? { provider: input.provider } : {}),
|
|
64
|
+
...(input.modelId ? { modelId: input.modelId } : {}),
|
|
65
|
+
configured: input.modelConfigured ?? false,
|
|
66
|
+
};
|
|
67
|
+
const authKind = input.authKind ?? "unknown";
|
|
68
|
+
const authState = authKind === "unknown" || authKind === "none" ? "unavailable" : "guaranteed";
|
|
69
|
+
if (authKind === "unknown")
|
|
70
|
+
degradedReasons.push(degradedReason("auth", "auth_unknown"));
|
|
71
|
+
if (authKind === "none")
|
|
72
|
+
degradedReasons.push(degradedReason("auth", "auth_unconfigured"));
|
|
73
|
+
const auth = { kind: authKind, origin: input.authOrigin ?? "unknown", state: authState };
|
|
74
|
+
const resumeAdvertised = input.resumeAdvertised ?? false;
|
|
75
|
+
const resumeState = resumeAdvertised ? "guaranteed" : "unavailable";
|
|
76
|
+
if (!resumeAdvertised)
|
|
77
|
+
degradedReasons.push(degradedReason("resume", "resume_unsupported"));
|
|
78
|
+
const resume = { advertised: resumeAdvertised, refIsPath: input.resumeRefIsPath ?? false, state: resumeState };
|
|
79
|
+
const toolEnforced = input.toolInterceptionEnforced ?? false;
|
|
80
|
+
const mcpOnly = !toolEnforced && Boolean(input.mcpToolApprovalsOnly);
|
|
81
|
+
const toolState = toolEnforced ? "guaranteed" : mcpOnly ? "degraded" : "unavailable";
|
|
82
|
+
if (toolState === "degraded")
|
|
83
|
+
degradedReasons.push(degradedReason("toolInterception", "tool_interception_mcp_only"));
|
|
84
|
+
if (toolState === "unavailable")
|
|
85
|
+
degradedReasons.push(degradedReason("toolInterception", "tool_interception_unavailable"));
|
|
86
|
+
const toolInterception = {
|
|
87
|
+
enforced: toolEnforced,
|
|
88
|
+
mcpOnly,
|
|
89
|
+
...(input.approvalMode ? { approvalMode: input.approvalMode } : {}),
|
|
90
|
+
state: toolState,
|
|
91
|
+
};
|
|
92
|
+
const runtimeEnforcement = input.runtimeEnforcement ?? "none";
|
|
93
|
+
const evidenceClass = runtimeEnforcement === "native-sandbox" ? "enforced" : runtimeEnforcement === "tool-controls" ? "observed" : "unavailable";
|
|
94
|
+
const sandboxState = evidenceClass === "enforced" ? "guaranteed" : evidenceClass === "observed" ? "degraded" : "unavailable";
|
|
95
|
+
if (sandboxState === "degraded")
|
|
96
|
+
degradedReasons.push(degradedReason("sandbox", "sandbox_observed_only"));
|
|
97
|
+
if (sandboxState === "unavailable")
|
|
98
|
+
degradedReasons.push(degradedReason("sandbox", "sandbox_unavailable"));
|
|
99
|
+
const sandbox = {
|
|
100
|
+
...(input.sandboxTier ? { tier: input.sandboxTier } : {}),
|
|
101
|
+
runtimeEnforcement,
|
|
102
|
+
evidenceClass,
|
|
103
|
+
state: sandboxState,
|
|
104
|
+
};
|
|
105
|
+
const supportTier = input.supportTier ?? "experimental";
|
|
106
|
+
const hasProtectionDegradation = degradedReasons.some((r) => PROTECTION_AREAS.has(r.area));
|
|
107
|
+
const requiresAcknowledgement = supportTier === "supported" && hasProtectionDegradation && !input.acknowledgedAt;
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: SESSION_CONTRACT_SCHEMA_VERSION,
|
|
110
|
+
resolvedAt: input.now,
|
|
111
|
+
preview: input.preview,
|
|
112
|
+
supportTier,
|
|
113
|
+
certification: input.certification ?? "unverified",
|
|
114
|
+
agent,
|
|
115
|
+
executionMode,
|
|
116
|
+
model,
|
|
117
|
+
auth,
|
|
118
|
+
resume,
|
|
119
|
+
toolInterception,
|
|
120
|
+
sandbox,
|
|
121
|
+
degradedReasons,
|
|
122
|
+
requiresAcknowledgement,
|
|
123
|
+
...(input.acknowledgedAt ? { acknowledgedAt: input.acknowledgedAt } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Map real node-side facts (a resolved `RuntimeInfo`, the chosen sandbox
|
|
128
|
+
* tier/approval mode, and whatever credential facts the caller has) into an
|
|
129
|
+
* Effective Session Contract. `authKind` defaults to "unknown" rather than
|
|
130
|
+
* guessing from `authOwner` — origin (bivy vault vs. the agent's own login)
|
|
131
|
+
* is a structural fact `authOwner` already reports honestly; the specific
|
|
132
|
+
* credential *kind* (api_key vs oauth) is not yet threaded through from
|
|
133
|
+
* every runtime's credential resolution and must not be invented.
|
|
134
|
+
*/
|
|
135
|
+
export function computeSessionContract(facts, now) {
|
|
136
|
+
const runtime = facts.runtime;
|
|
137
|
+
const caps = runtime.capabilities ?? {};
|
|
138
|
+
const authOrigin = runtime.authOwner === "bivy" ? "bivy" : runtime.authOwner === "agent" ? "agent-native" : "unknown";
|
|
139
|
+
return resolveSessionContract({
|
|
140
|
+
now,
|
|
141
|
+
preview: facts.preview,
|
|
142
|
+
agentId: runtime.id,
|
|
143
|
+
agentDisplayName: runtime.displayName,
|
|
144
|
+
detectedVersion: runtime.testedVersion,
|
|
145
|
+
versionSource: runtime.testedVersion ? "tested-pin" : undefined,
|
|
146
|
+
supportTier: runtime.supportTier,
|
|
147
|
+
certification: runtime.certification,
|
|
148
|
+
executionMode: runtime.executionMode,
|
|
149
|
+
provider: facts.provider,
|
|
150
|
+
modelId: facts.modelId,
|
|
151
|
+
modelConfigured: facts.modelConfigured,
|
|
152
|
+
authKind: facts.authKind,
|
|
153
|
+
authOrigin,
|
|
154
|
+
resumeAdvertised: caps.resume === true,
|
|
155
|
+
resumeRefIsPath: caps.sessionRefIsPath === true,
|
|
156
|
+
toolInterceptionEnforced: caps.toolInterception === true,
|
|
157
|
+
mcpToolApprovalsOnly: caps.mcpToolApprovals === true,
|
|
158
|
+
approvalMode: facts.approvalMode,
|
|
159
|
+
sandboxTier: facts.sandbox,
|
|
160
|
+
runtimeEnforcement: runtime.protectionLevel,
|
|
161
|
+
acknowledgedAt: facts.acknowledgedAt,
|
|
162
|
+
});
|
|
163
|
+
}
|
package/package.json
CHANGED