| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import { isThinkingLevel } from '../model-thinking.js'; |
| import { CHAT_DEFAULT_PERMISSION_MODES } from '../settings.js'; |
| import { normalizeSubagentSettings } from '../subagent-settings.js'; |
| import type { |
| AgentRuntimeSettingsPatch, |
| MutateRuntimePolicyInput, |
| NetworkProxyCredentialUpdate, |
| NetworkProxyCredentialTarget, |
| RuntimePolicy, |
| RuntimePolicyMutation, |
| UpdateNetworkProxyInput, |
| } from '../runtime-policy.js'; |
| import { |
| decodeCredentialVersionBasis, |
| normalizeCredentialSecret, |
| } from './credential-vault-codec.js'; |
| import { WEB_SEARCH_PROVIDERS } from '../web-search.js'; |
| import { |
| assertCanonicalValue, |
| booleanValue, |
| domainError, |
| exactRecord, |
| integerValue, |
| revisionValue, |
| stringArrayValue, |
| stringValue, |
| } from './domain-codec.js'; |
| |
| export function decodeCanonicalRuntimePolicy(value: unknown): RuntimePolicy { |
| const decoded = normalizeRuntimePolicy(value); |
| assertCanonicalValue(value, decoded, 'runtime policy'); |
| return decoded; |
| } |
| |
| /** Upgrade the immediately previous canonical document with the new shell default. */ |
| export function decodeRuntimePolicyV2(value: unknown): RuntimePolicy { |
| const policy = exactRecord(value, 'runtime policy v2', [ |
| 'networkProxy', |
| 'personalization', |
| 'memory', |
| 'workspaceInstructions', |
| 'privacy', |
| 'chatDefaults', |
| 'webSearch', |
| 'subagents', |
| ]); |
| const decoded = normalizeRuntimePolicyFields( |
| policy, |
| normalizeSubagentSettings(policy.subagents), |
| { preference: 'auto', executable: '' }, |
| ); |
| assertCanonicalValue(value, withoutExternalAgents(withoutShell(decoded)), 'runtime policy v2'); |
| return decoded; |
| } |
| |
| export function normalizeRuntimePolicyMutation(value: unknown): MutateRuntimePolicyInput { |
| const input = exactRecord(value, 'runtime policy mutation', ['expectedRevision', 'operation']); |
| const operation = exactRecord(input.operation, 'runtime policy operation', ['kind', 'value']); |
| return { |
| expectedRevision: revisionValue(input.expectedRevision, 'runtime policy expected revision'), |
| operation: normalizeMutationOperation(operation), |
| }; |
| } |
| |
| export function normalizeNetworkProxyUpdate(value: unknown): UpdateNetworkProxyInput { |
| const input = exactRecord(value, 'network proxy update', [ |
| 'expectedPolicyRevision', |
| 'expectedCredential', |
| 'networkProxy', |
| 'credential', |
| ]); |
| const expectedCredential = |
| input.expectedCredential === null |
| ? null |
| : decodeCredentialVersionBasis(input.expectedCredential); |
| if (expectedCredential !== null && expectedCredential.locator.scope !== 'network_proxy') { |
| throw domainError('network proxy update requires a network proxy credential basis'); |
| } |
| const credential = normalizeNetworkProxyCredentialUpdate(input.credential); |
| const networkProxy = normalizeNetworkProxy(input.networkProxy); |
| if (!networkProxy.authEnabled && credential.kind !== 'delete') { |
| throw domainError('disabled proxy authentication requires credential deletion'); |
| } |
| return { |
| expectedPolicyRevision: revisionValue( |
| input.expectedPolicyRevision, |
| 'network proxy expected policy revision', |
| ), |
| expectedCredential, |
| networkProxy, |
| credential, |
| }; |
| } |
| |
| function normalizeNetworkProxyCredentialUpdate(value: unknown): NetworkProxyCredentialUpdate { |
| const base = exactRecord( |
| value, |
| 'network proxy credential update', |
| ['kind', 'secret', 'expectedTarget'], |
| ['kind'], |
| ); |
| switch (base.kind) { |
| case 'keep': |
| case 'delete': { |
| exactRecord(value, `network proxy credential ${base.kind}`, ['kind']); |
| return { kind: base.kind }; |
| } |
| case 'replace': { |
| const replacement = exactRecord( |
| value, |
| 'network proxy credential replacement', |
| ['kind', 'secret', 'expectedTarget'], |
| ['kind', 'secret'], |
| ); |
| return { |
| kind: 'replace', |
| secret: normalizeCredentialSecret(replacement.secret), |
| ...(replacement.expectedTarget === undefined |
| ? {} |
| : { |
| expectedTarget: normalizeNetworkProxyCredentialTarget(replacement.expectedTarget), |
| }), |
| }; |
| } |
| default: |
| throw domainError(`network proxy credential update '${String(base.kind)}' is unknown`); |
| } |
| } |
| |
| export function normalizeNetworkProxyCredentialTarget( |
| value: unknown, |
| ): NetworkProxyCredentialTarget { |
| const item = exactRecord(value, 'network proxy credential target', [ |
| 'protocol', |
| 'host', |
| 'port', |
| 'username', |
| ]); |
| if (item.protocol !== 'http' && item.protocol !== 'https' && item.protocol !== 'socks5') { |
| throw domainError('network proxy credential target protocol is invalid'); |
| } |
| const rawHost = stringValue(item.host, 'network proxy credential target host', 255); |
| if (/[\u0000-\u001f\u007f-\u009f]/.test(rawHost)) { |
| throw domainError('network proxy credential target host must not contain control characters'); |
| } |
| const host = rawHost.trim().toLowerCase(); |
| if (host.length === 0) { |
| throw domainError('network proxy credential target host must not be empty'); |
| } |
| return { |
| protocol: item.protocol, |
| host, |
| port: integerValue(item.port, 'network proxy credential target port', 1, 65_535), |
| username: stringValue(item.username, 'network proxy credential target username', 256), |
| }; |
| } |
| |
| function normalizeRuntimePolicy(value: unknown): RuntimePolicy { |
| const policy = exactRecord(value, 'runtime policy', [ |
| 'networkProxy', |
| 'personalization', |
| 'memory', |
| 'workspaceInstructions', |
| 'privacy', |
| 'chatDefaults', |
| 'webSearch', |
| 'subagents', |
| 'shell', |
| 'externalAgents', |
| ]); |
| return normalizeRuntimePolicyFields( |
| policy, |
| normalizeSubagentSettings(policy.subagents), |
| normalizeShell(policy.shell), |
| normalizeExternalAgents(policy.externalAgents), |
| ); |
| } |
| |
| function normalizeRuntimePolicyFields( |
| policy: Record<string, unknown>, |
| subagents: RuntimePolicy['subagents'], |
| shell: RuntimePolicy['shell'], |
| externalAgents: RuntimePolicy['externalAgents'] = { antigravity: { executable: '' } }, |
| ): RuntimePolicy { |
| return { |
| networkProxy: normalizeNetworkProxy(policy.networkProxy), |
| personalization: normalizePersonalization(policy.personalization), |
| memory: normalizeMemory(policy.memory), |
| workspaceInstructions: normalizeWorkspaceInstructions(policy.workspaceInstructions), |
| privacy: normalizePrivacy(policy.privacy), |
| chatDefaults: normalizeChatDefaults(policy.chatDefaults), |
| webSearch: normalizeWebSearch(policy.webSearch), |
| subagents, |
| shell, |
| externalAgents, |
| }; |
| } |
| |
| function withoutShell(policy: RuntimePolicy): Omit<RuntimePolicy, 'shell'> { |
| const { shell: _shell, ...legacy } = policy; |
| return legacy; |
| } |
| |
| function normalizeMutationOperation(operation: Record<string, unknown>): RuntimePolicyMutation { |
| switch (operation.kind) { |
| case 'set_network_proxy': |
| return { kind: operation.kind, value: normalizeNetworkProxy(operation.value) }; |
| case 'set_personalization': |
| return { kind: operation.kind, value: normalizePersonalization(operation.value) }; |
| case 'set_memory': |
| return { kind: operation.kind, value: normalizeMemory(operation.value) }; |
| case 'set_workspace_instructions': |
| return { kind: operation.kind, value: normalizeWorkspaceInstructions(operation.value) }; |
| case 'set_privacy': |
| return { kind: operation.kind, value: normalizePrivacy(operation.value) }; |
| case 'set_chat_defaults': |
| return { kind: operation.kind, value: normalizeChatDefaults(operation.value) }; |
| case 'set_web_search': |
| return { kind: operation.kind, value: normalizeWebSearch(operation.value) }; |
| case 'set_subagents': |
| return { kind: operation.kind, value: normalizeSubagentSettings(operation.value) }; |
| case 'set_external_agents': |
| return { kind: operation.kind, value: normalizeExternalAgents(operation.value) }; |
| case 'set_shell': |
| return { kind: operation.kind, value: normalizeShell(operation.value) }; |
| case 'patch_agent_settings': |
| return { kind: operation.kind, value: normalizeAgentRuntimeSettingsPatch(operation.value) }; |
| default: |
| throw domainError(`runtime policy operation '${String(operation.kind)}' is unknown`); |
| } |
| } |
| |
| function normalizeShell(value: unknown): RuntimePolicy['shell'] { |
| const item = exactRecord(value, 'shell policy', ['preference', 'executable']); |
| if (item.preference !== 'auto' && item.preference !== 'git_bash') { |
| throw domainError('shell preference is invalid'); |
| } |
| const executable = stringValue(item.executable, 'shell executable', 4_096).trim(); |
| if (/[\u0000-\u001f\u007f-\u009f]/.test(executable)) { |
| throw domainError('shell executable must not contain control characters'); |
| } |
| if (item.preference === 'git_bash' && executable.length === 0) { |
| throw domainError('Git Bash executable must not be empty'); |
| } |
| return { preference: item.preference, executable }; |
| } |
| |
| function normalizeAgentRuntimeSettingsPatch(value: unknown): AgentRuntimeSettingsPatch { |
| const patch = exactRecord( |
| value, |
| 'agent runtime settings patch', |
| ['personalization', 'memory', 'workspaceInstructions', 'privacy', 'webSearch'], |
| [], |
| ); |
| return { |
| ...(patch.personalization === undefined |
| ? {} |
| : { personalization: normalizePersonalizationPatch(patch.personalization) }), |
| ...(patch.memory === undefined ? {} : { memory: normalizeMemoryPatch(patch.memory) }), |
| ...(patch.workspaceInstructions === undefined |
| ? {} |
| : { |
| workspaceInstructions: normalizeEnabledPatch( |
| patch.workspaceInstructions, |
| 'workspace instructions patch', |
| ), |
| }), |
| ...(patch.privacy === undefined ? {} : { privacy: normalizePrivacyPatch(patch.privacy) }), |
| ...(patch.webSearch === undefined |
| ? {} |
| : { webSearch: normalizeEnabledPatch(patch.webSearch, 'web search patch') }), |
| }; |
| } |
| |
| function normalizePersonalizationPatch( |
| value: unknown, |
| ): AgentRuntimeSettingsPatch['personalization'] { |
| const patch = exactRecord(value, 'personalization patch', ['displayName', 'assistantTone'], []); |
| return { |
| ...(patch.displayName === undefined |
| ? {} |
| : { displayName: stringValue(patch.displayName, 'personalization displayName', 256) }), |
| ...(patch.assistantTone === undefined |
| ? {} |
| : { |
| assistantTone: stringValue(patch.assistantTone, 'personalization assistantTone', 4_096), |
| }), |
| }; |
| } |
| |
| function normalizeMemoryPatch(value: unknown): AgentRuntimeSettingsPatch['memory'] { |
| const patch = exactRecord(value, 'memory patch', ['enabled', 'agentReadEnabled'], []); |
| return { |
| ...(patch.enabled === undefined |
| ? {} |
| : { enabled: booleanValue(patch.enabled, 'memory enabled') }), |
| ...(patch.agentReadEnabled === undefined |
| ? {} |
| : { |
| agentReadEnabled: booleanValue(patch.agentReadEnabled, 'memory agentReadEnabled'), |
| }), |
| }; |
| } |
| |
| function normalizePrivacyPatch(value: unknown): AgentRuntimeSettingsPatch['privacy'] { |
| const patch = exactRecord(value, 'privacy patch', ['incognitoActive'], []); |
| return { |
| ...(patch.incognitoActive === undefined |
| ? {} |
| : { incognitoActive: booleanValue(patch.incognitoActive, 'privacy incognitoActive') }), |
| }; |
| } |
| |
| function normalizeEnabledPatch(value: unknown, name: string): { readonly enabled?: boolean } { |
| const patch = exactRecord(value, name, ['enabled'], []); |
| return { |
| ...(patch.enabled === undefined |
| ? {} |
| : { enabled: booleanValue(patch.enabled, `${name} enabled`) }), |
| }; |
| } |
| |
| function normalizeNetworkProxy(value: unknown): RuntimePolicy['networkProxy'] { |
| const item = exactRecord(value, 'network proxy', [ |
| 'enabled', |
| 'protocol', |
| 'host', |
| 'port', |
| 'authEnabled', |
| 'username', |
| 'bypassList', |
| 'autoBypassDomains', |
| ]); |
| const enabled = booleanValue(item.enabled, 'network proxy enabled'); |
| const rawHost = stringValue(item.host, 'network proxy host', 255); |
| if (/[\u0000-\u001f\u007f-\u009f]/.test(rawHost)) { |
| throw domainError('network proxy host must not contain control characters'); |
| } |
| const host = rawHost.trim(); |
| if (enabled && host.length === 0) { |
| throw domainError('network proxy host must not be empty when enabled'); |
| } |
| if (item.protocol !== 'http' && item.protocol !== 'https' && item.protocol !== 'socks5') { |
| throw domainError('network proxy protocol is invalid'); |
| } |
| return { |
| enabled, |
| protocol: item.protocol, |
| host, |
| port: integerValue(item.port, 'network proxy port', 1, 65_535), |
| authEnabled: booleanValue(item.authEnabled, 'network proxy authEnabled'), |
| username: stringValue(item.username, 'network proxy username', 256), |
| bypassList: stringArrayValue(item.bypassList, 'network proxy bypassList', 256), |
| autoBypassDomains: stringArrayValue( |
| item.autoBypassDomains, |
| 'network proxy autoBypassDomains', |
| 256, |
| ), |
| }; |
| } |
| |
| function normalizePersonalization(value: unknown): RuntimePolicy['personalization'] { |
| const item = exactRecord(value, 'personalization', ['displayName', 'assistantTone']); |
| return { |
| displayName: stringValue(item.displayName, 'personalization displayName', 256), |
| assistantTone: stringValue(item.assistantTone, 'personalization assistantTone', 4_096), |
| }; |
| } |
| |
| function normalizeMemory(value: unknown): RuntimePolicy['memory'] { |
| const item = exactRecord(value, 'memory policy', ['enabled', 'agentReadEnabled']); |
| return { |
| enabled: booleanValue(item.enabled, 'memory enabled'), |
| agentReadEnabled: booleanValue(item.agentReadEnabled, 'memory agentReadEnabled'), |
| }; |
| } |
| |
| function normalizeWorkspaceInstructions(value: unknown): RuntimePolicy['workspaceInstructions'] { |
| const item = exactRecord(value, 'workspace instructions policy', ['enabled']); |
| return { enabled: booleanValue(item.enabled, 'workspace instructions enabled') }; |
| } |
| |
| function normalizePrivacy(value: unknown): RuntimePolicy['privacy'] { |
| const item = exactRecord(value, 'privacy policy', ['incognitoActive']); |
| return { incognitoActive: booleanValue(item.incognitoActive, 'privacy incognitoActive') }; |
| } |
| |
| function normalizeChatDefaults(value: unknown): RuntimePolicy['chatDefaults'] { |
| const item = exactRecord( |
| value, |
| 'chat defaults', |
| ['permissionMode', 'thinkingLevel', 'codeModeEnabled'], |
| ['permissionMode'], |
| ); |
| if (!(CHAT_DEFAULT_PERMISSION_MODES as readonly unknown[]).includes(item.permissionMode)) { |
| throw domainError('chat default permission mode is invalid'); |
| } |
| if (item.thinkingLevel !== undefined && !isThinkingLevel(item.thinkingLevel)) { |
| throw domainError('chat default thinking level is invalid'); |
| } |
| if (item.codeModeEnabled !== undefined && typeof item.codeModeEnabled !== 'boolean') { |
| throw domainError('chat default code mode is invalid'); |
| } |
| return { |
| permissionMode: item.permissionMode as RuntimePolicy['chatDefaults']['permissionMode'], |
| ...(item.codeModeEnabled === true ? { codeModeEnabled: true } : {}), |
| ...(item.thinkingLevel === undefined ? {} : { thinkingLevel: item.thinkingLevel }), |
| }; |
| } |
| |
| function normalizeWebSearch(value: unknown): RuntimePolicy['webSearch'] { |
| const item = exactRecord(value, 'web search policy', ['enabled', 'defaultProvider']); |
| if (!(WEB_SEARCH_PROVIDERS as readonly unknown[]).includes(item.defaultProvider)) { |
| throw domainError('web search default provider is invalid'); |
| } |
| return { |
| enabled: booleanValue(item.enabled, 'web search enabled'), |
| defaultProvider: item.defaultProvider as RuntimePolicy['webSearch']['defaultProvider'], |
| }; |
| } |
| |
| /** Read the previous document without loosening the current wire decoder. */ |
| export function decodeRuntimePolicyV3(value: unknown): RuntimePolicy { |
| const old = exactRecord(value, 'runtime policy v3', [ |
| 'networkProxy', |
| 'personalization', |
| 'memory', |
| 'workspaceInstructions', |
| 'privacy', |
| 'chatDefaults', |
| 'webSearch', |
| 'subagents', |
| 'shell', |
| ]); |
| return decodeCanonicalRuntimePolicy({ |
| ...old, |
| externalAgents: { antigravity: { executable: '' } }, |
| }); |
| } |
| |
| function withoutExternalAgents<T extends { externalAgents: RuntimePolicy['externalAgents'] }>( |
| policy: T, |
| ): Omit<T, 'externalAgents'> { |
| const { externalAgents: _externalAgents, ...legacy } = policy; |
| return legacy; |
| } |
| |
| function normalizeExternalAgents(value: unknown): RuntimePolicy['externalAgents'] { |
| const agents = exactRecord(value, 'external agents', ['antigravity']); |
| const agent = exactRecord(agents.antigravity, 'Antigravity', ['executable']); |
| const executable = stringValue(agent.executable, 'Antigravity executable', 4096); |
| if (executable !== '' && (!executable.startsWith('/') || /[\x00-\x1f]/u.test(executable))) { |
| throw domainError('Antigravity executable must be an absolute macOS path'); |
| } |
| return { antigravity: { executable } }; |
| } |