跳至主要内容

在 Copilot CLI 中使用钩子实现可预测、符合政策的执行

使用钩子记录用户提示并控制 Copilot CLI 在仓库中可以运行的工具,从而让团队在组织的安全与合规要求下安全地实现自动化。

本教程面向支持使用 Copilot CLI 的开发者的 DevOps 工程师、平台团队和技术主管。

钩子是自定义脚本,会在 Copilot CLI 会话的特定阶段运行。它们可以检查提示和工具调用、记录审计信息,甚至阻止某些命令的执行。

你将配置作用于仓库范围的钩子,这些钩子

  • 提供对提示和工具使用的可视化。
  • 在执行前阻止高风险的命令模式。
  • 通过明确的信息帮助开发者了解组织政策。

先决条件

  • 熟悉 Shell 脚本(Bash 或 PowerShell)
  • 对 JSON 配置文件有基本了解
  • 能够访问使用 Copilot CLI 的仓库
  • jq 已安装(用于 Bash 示例)

1. 定义组织政策

在编写任何钩子脚本之前,先决定哪些操作应自动允许,哪些需要人工审查。

明确的政策帮助你在避免过度拦截的同时仍能降低风险。

识别始终需要审查的命令

首先找出绝不应由 Copilot CLI 自动执行的模式。常见示例包括:

  • 特权提升sudosurunas
  • 破坏性系统操作rm -rf /mkfsddformat
  • 下载后执行模式curl ... | bashwget ... | sh、PowerShell iex (irm ...)

如果这些命令被意外执行,会产生不可逆的影响。

决定记录哪些内容

使用钩子时,你可以捕获 Copilot CLI 在仓库中的使用情况,包括用户提交的提示以及 Copilot CLI 试图运行的工具。

大多数组织至少记录以下信息:

  • 时间戳和仓库路径
  • 提示文本(或其脱敏形式)
  • 工具名称和工具参数
  • 任何政策决策(例如,被拒绝的命令及其原因)

避免记录机密或凭证。如果提示或命令可能包含敏感数据,请在写入日志前进行脱敏。

本教程使用本地 .github/hooks/logs 目录作为一个简单的示例。这些日志文件 不应提交到仓库,通常只保存在开发者的机器上。

在生产环境中,许多组织会把钩子事件转发到集中式日志或可观测性系统,而不是本地写日志。这让团队能够在所有仓库和用户之间统一执行脱敏、访问控制、保留策略和监控。

与相关方保持一致

在强制执行政策之前,请与以下团队一起评审:

  • 安全或合规团队,以确认风险边界
  • 平台或基础设施团队,可能需要更宽松的权限
  • 开发团队,让他们了解哪些会被拦截以及原因

明确的期望让政策执行更容易被采纳和维护。

2. 设置仓库钩子文件

在本教程中,你将使用存放在仓库 .github/hooks/ 下的 仓库范围钩子。这些钩子会在任何从该仓库运行的 Copilot CLI 会话中生效。

注意

Copilot 代理会从仓库中的 .github/hooks/*.json 加载钩子配置文件。钩子同步执行,并且可以阻止后续操作。

创建目录结构

在仓库根目录下,创建用于钩子配置、脚本和日志的目录。

Bash
mkdir -p .github/hooks/scripts
mkdir -p .github/hooks/logs

.github/hooks/logs/ 加入 .gitignore,以防本地审计日志被提交。

Bash
echo ".github/hooks/logs/" >> .gitignore

本教程使用以下结构:

.github/
└── hooks/
    ├── copilot-cli-policy.json
    ├── logs/
    │   └── audit.jsonl
    └── scripts/
        ├── session-banner.sh
        ├── session-banner.ps1
        ├── log-prompt.sh
        ├── log-prompt.ps1
        ├── pre-tool-policy.sh
        └── pre-tool-policy.ps1

创建钩子配置文件

.github/hooks/copilot-cli-policy.json 处创建钩子配置文件。

该文件定义了哪些钩子会运行、何时运行以及执行哪些脚本。

JSON
{
  "version": 1,
  "hooks": {
    "sessionStart": [
      {
        "type": "command",
        "bash": "./scripts/session-banner.sh",
        "powershell": "./scripts/session-banner.ps1",
        "cwd": ".github/hooks",
        "timeoutSec": 10
      }
    ],
    "userPromptSubmitted": [
      {
        "type": "command",
        "bash": "./scripts/log-prompt.sh",
        "powershell": "./scripts/log-prompt.ps1",
        "cwd": ".github/hooks",
        "timeoutSec": 10
      }
    ],
    "preToolUse": [
      {
        "type": "command",
        "bash": "./scripts/pre-tool-policy.sh",
        "powershell": "./scripts/pre-tool-policy.ps1",
        "cwd": ".github/hooks",
        "timeoutSec": 15
      }
    ]
  }
}

了解此配置的作用

此配置设置了三个钩子:

  • sessionStart:在新的代理会话启动或恢复时显示信息横幅。
  • userPromptSubmitted:在用户提交提示时运行。
  • preToolUse:在工具执行前运行,可显式允许或拒绝执行。

提交并共享钩子配置

当你准备将钩子配置与协作者共享(例如通过 Pull Request 或在测试仓库中),请提交配置文件和脚本。不要提交本地审计日志。

Bash
git add .github/hooks/copilot-cli-policy.json .github/hooks/scripts
git commit -m "Add Copilot CLI hook configuration"
git push

此时,Copilot CLI 已经可以发现你的钩子配置,尽管你尚未创建具体的钩子脚本。

3. 在会话开始时添加政策横幅

使用 sessionStart 钩子在每次新建或恢复 Copilot CLI 会话时显示横幅,让开发者明确组织政策已生效。

sessionStart 钩子会收到当前工作目录、初始提示等上下文信息。该钩子的任何输出都会被 Copilot CLI 忽略,适合作为信息提示。

创建会话横幅脚本(Bash)

创建 .github/hooks/scripts/session-banner.sh

Bash
#!/bin/bash
set -euo pipefail

cat << 'EOF'
COPILOT CLI POLICY ACTIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• Prompts and tool use may be logged for auditing
• High-risk commands may be blocked automatically
• If something is blocked, follow the guidance shown
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0

创建会话横幅脚本(PowerShell)

创建 .github/hooks/scripts/session-banner.ps1

PowerShell
$ErrorActionPreference = "Stop"

Write-Host @"
COPILOT CLI POLICY ACTIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• Prompts and tool use may be logged for auditing
• High-risk commands may be blocked automatically
• If something is blocked, follow the guidance shown
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"@
exit 0

测试会话横幅

可以直接运行横幅脚本进行测试。

.github/hooks/scripts/session-banner.sh
# or, for PowerShell
.github/hooks/scripts/session-banner.ps1

运行任意脚本时,你应该在终端看到政策横幅显示。

4. 记录提示以供审计

使用 userPromptSubmitted 钩子记录用户向 Copilot CLI 提交的提示。该钩子在任何工具被调用之前运行。

钩子会接收结构化的 JSON 输入,包含时间戳、当前工作目录以及完整提示文本。钩子的输出同样会被忽略。

重要提示

提示可能包含敏感信息。记录时请先脱敏,并遵守组织的数据处理与保留政策。

创建提示记录脚本(Bash)

创建 .github/hooks/scripts/log-prompt.sh

Bash
#!/bin/bash
set -euo pipefail

INPUT="$(cat)"

TIMESTAMP_MS="$(echo "$INPUT" | jq -r '.timestamp // empty')"
CWD="$(echo "$INPUT" | jq -r '.cwd // empty')"

# This example logs only metadata, not the full prompt, to avoid storing
# potentially sensitive data. Adjust to match your organization’s needs.
LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"
chmod 700 "$LOG_DIR"

jq -n \
  --arg ts "$TIMESTAMP_MS" \
  --arg cwd "$CWD" \
  '{event:"userPromptSubmitted", timestampMs:$ts, cwd:$cwd}' \
  >> "$LOG_DIR/audit.jsonl"

exit 0

创建提示记录脚本(PowerShell)

创建 .github/hooks/scripts/log-prompt.ps1

PowerShell
$ErrorActionPreference = "Stop"

$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json

$timestampMs = $inputObj.timestamp
$cwd = $inputObj.cwd
$prompt = $inputObj.prompt

# Optional example redaction. Adjust to match your organization’s needs.
$redactedPrompt = $prompt -replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]'

$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) {
  New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}

$logEntry = @{
  event       = "userPromptSubmitted"
  timestampMs = $timestampMs
  cwd         = $cwd
  prompt      = $redactedPrompt
} | ConvertTo-Json -Compress

Add-Content -Path "$logDir/audit.jsonl" -Value $logEntry
exit 0

测试提示记录脚本

可以通过管道传入示例输入直接测试脚本。

echo '{"timestamp":1704614500000,"cwd":"/repo","prompt":"List all branches"}' \
  | .github/hooks/scripts/log-prompt.sh
# or, for PowerShell
echo '{"timestamp":1704614500000,"cwd":"/repo","prompt":"List all branches"}' |
  .github/hooks/scripts/log-prompt.ps1

运行脚本后,请检查 .github/hooks/logs/audit.jsonl 是否出现新的日志条目。

Bash
cat .github/hooks/logs/audit.jsonl

此时,提交到 Copilot CLI 的提示已被记录用于审计。

5. 使用 preToolUse 强制执行政策

使用 preToolUse 钩子在 工具实际执行前 评估调用。该钩子可以通过不返回内容来允许执行,或返回结构化响应来拒绝执行。

了解 preToolUse 输入

preToolUse 钩子的输入包括:

  • toolName:Copilot CLI 即将运行的工具名称(例如 bash
  • toolArgs:该工具参数的 JSON 字符串

因为 toolArgs 是 JSON 字符串,脚本必须先将其解析,才能读取如 command 等字段。

重要提示

工具参数和命令可能包含 API 令牌、密码或其他凭证等敏感信息。记录前请先脱敏,并遵循组织的安全政策。建议仅记录非敏感元数据(工具名、时间戳、政策决策),并将审计事件发送到具备访问控制和保留策略的安全集中式日志系统。

创建政策脚本

接下来,创建一个政策脚本。此示例:

  • 记录所有尝试使用的工具。
  • 仅对 Bash 命令应用拒绝规则。
  • 阻止高风险模式,如特权提升、破坏性操作以及下载后执行的命令。

为了让你安全地验证拒绝流程,脚本中还包含一个临时演示规则,阻止一个无害的测试命令。确认钩子按预期工作后,请删除演示规则并替换为符合组织政策的模式。

示例脚本(Bash)

创建 .github/hooks/scripts/pre-tool-policy.sh

Bash
#!/bin/bash
set -euo pipefail

INPUT="$(cat)"

TOOL_NAME="$(echo "$INPUT" | jq -r '.toolName // empty')"
TOOL_ARGS_RAW="$(echo "$INPUT" | jq -r '.toolArgs // empty')"  # JSON string

LOG_DIR=".github/hooks/logs"
mkdir -p "$LOG_DIR"

# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
REDACTED_TOOL_ARGS="$(echo "$TOOL_ARGS_RAW" | \
  sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
  sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
  sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
  sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
  sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
  sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
  sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"

# Log attempted tool use with redacted toolArgs.
jq -n \
  --arg tool "$TOOL_NAME" \
  --arg toolArgs "$REDACTED_TOOL_ARGS" \
  '{event:"preToolUse", toolName:$tool, toolArgs:$toolArgs}' \
  >> "$LOG_DIR/audit.jsonl"

# Only enforce command rules for bash.
if [ "$TOOL_NAME" != "bash" ]; then
  exit 0
fi

# Parse toolArgs JSON string.
# If toolArgs isn't valid JSON for some reason, allow (and rely on logs).
if ! echo "$TOOL_ARGS_RAW" | jq -e . >/dev/null 2>&1; then
  exit 0
fi

COMMAND="$(echo "$TOOL_ARGS_RAW" | jq -r '.command // empty')"

# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if echo "$COMMAND" | grep -q "COPILOT_HOOKS_DENY_DEMO"; then
  deny "Blocked demo command (test rule). Remove this rule after validating hooks."
fi

deny() {
  local reason="$1"

  # Redact sensitive patterns from command before logging.
  local redacted_cmd="$(echo "$COMMAND" | \
    sed -E 's/ghp_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
    sed -E 's/gho_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
    sed -E 's/ghu_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
    sed -E 's/ghs_[A-Za-z0-9]{20,}/[REDACTED_TOKEN]/g' | \
    sed -E 's/Bearer [A-Za-z0-9_\-\.]+/Bearer [REDACTED]/g' | \
    sed -E 's/--password[= ][^ ]+/--password=[REDACTED]/g' | \
    sed -E 's/--token[= ][^ ]+/--token=[REDACTED]/g')"

  # Log the denial decision with redacted command.
  jq -n \
    --arg cmd "$redacted_cmd" \
    --arg r "$reason" \
    '{event:"policyDeny", toolName:"bash", command:$cmd, reason:$r}' \
    >> "$LOG_DIR/audit.jsonl"

  # Return a denial response.
  jq -n \
    --arg r "$reason" \
    '{permissionDecision:"deny", permissionDecisionReason:$r}'

  exit 0
}

# Privilege escalation
if echo "$COMMAND" | grep -qE '\b(sudo|su|runas)\b'; then
  deny "Privilege escalation requires manual approval."
fi

# Destructive filesystem operations targeting root
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s*/($|\s)|rm\s+.*-rf\s*/($|\s)'; then
  deny "Destructive operations targeting the filesystem root require manual approval."
fi

# System-level destructive operations
if echo "$COMMAND" | grep -qE '\b(mkfs|dd|format)\b'; then
  deny "System-level destructive operations are not allowed via automated execution."
fi

# Download-and-execute patterns
if echo "$COMMAND" | grep -qE 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)'; then
  deny "Download-and-execute patterns require manual approval."
fi

# Allow by default
exit 0

创建政策脚本(PowerShell)

创建 .github/hooks/scripts/pre-tool-policy.ps1

PowerShell
$ErrorActionPreference = "Stop"

$inputObj = [Console]::In.ReadToEnd() | ConvertFrom-Json
$toolName = $inputObj.toolName
$toolArgsRaw = $inputObj.toolArgs  # JSON string

$logDir = ".github/hooks/logs"
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }

# Example redaction logic.
# GitHub does not currently provide built-in secret redaction for hooks.
# This example shows one possible approach; many organizations prefer to
# forward events to a centralized logging system that handles redaction.
# Redact sensitive patterns before logging.
# Adjust these patterns to match your organization's needs.
$redactedToolArgs = $toolArgsRaw `
  -replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
  -replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
  -replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
  -replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
  -replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
  -replace '--password[= ][^ ]+', '--password=[REDACTED]' `
  -replace '--token[= ][^ ]+', '--token=[REDACTED]'

# Log attempted tool use with redacted toolArgs.
(@{
  event    = "preToolUse"
  toolName = $toolName
  toolArgs = $redactedToolArgs
} | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"

if ($toolName -ne "bash") { exit 0 }

# Parse toolArgs JSON string.
$toolArgs = $null
try { $toolArgs = $toolArgsRaw | ConvertFrom-Json } catch { exit 0 }

$command = $toolArgs.command

# ---------------------------------------------------------------------------
# Demo-only deny rule for safe testing.
# This blocks a harmless test command so you can validate the deny flow.
# Remove this rule after confirming your hooks work as expected.
# ---------------------------------------------------------------------------
if ($command -match 'COPILOT_HOOKS_DENY_DEMO') {
  Deny "Blocked demo command (test rule). Remove this rule after validating hooks."
}

function Deny([string]$reason) {
  # Redact sensitive patterns from command before logging.
  $redactedCommand = $command `
    -replace 'ghp_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
    -replace 'gho_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
    -replace 'ghu_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
    -replace 'ghs_[A-Za-z0-9]{20,}', '[REDACTED_TOKEN]' `
    -replace 'Bearer [A-Za-z0-9_\-\.]+', 'Bearer [REDACTED]' `
    -replace '--password[= ][^ ]+', '--password=[REDACTED]' `
    -replace '--token[= ][^ ]+', '--token=[REDACTED]'

  # Log the denial decision with redacted command.
  (@{
    event    = "policyDeny"
    toolName = "bash"
    command  = $redactedCommand
    reason   = $reason
  } | ConvertTo-Json -Compress) | Add-Content -Path "$logDir/audit.jsonl"

  (@{
    permissionDecision = "deny"
    permissionDecisionReason = $reason
  } | ConvertTo-Json -Compress)

  exit 0
}

if ($command -match '\b(sudo|su|runas)\b') { Deny "Privilege escalation requires manual approval." }
if ($command -match 'rm\s+-rf\s*/(\s|$)|rm\s+.*-rf\s*/(\s|$)') { Deny "Destructive operations targeting the filesystem root require manual approval." }
if ($command -match '\b(mkfs|dd|format)\b') { Deny "System-level destructive operations are not allowed via automated execution." }
if ($command -match 'curl.*\|\s*(bash|sh)|wget.*\|\s*(bash|sh)') { Deny "Download-and-execute patterns require manual approval." }

exit 0

测试政策脚本

可以通过管道传入示例的 preToolUse 输入来测试脚本。

允许示例

echo '{"toolName":"bash","toolArgs":"{\"command\":\"git status\"}"}' \
  | .github/hooks/scripts/pre-tool-policy.sh
# or, for PowerShell
echo '{"toolName":"bash","toolArgs":"{\"command\":\"git status\"}"}' |
  .github/hooks/scripts/pre-tool-policy.ps1

拒绝示例

echo '{"toolName":"bash","toolArgs":"{\"command\":\"sudo rm -rf /\"}"}' \
  | .github/hooks/scripts/pre-tool-policy.sh
# or, for PowerShell
echo '{"toolName":"bash","toolArgs":"{\"command\":\"sudo rm -rf /\"}"}' |
  .github/hooks/scripts/pre-tool-policy.ps1

运行拒绝示例后,请检查 .github/hooks/logs/audit.jsonl 是否出现新的拒绝日志条目。

{"permissionDecision":"deny","permissionDecisionReason":"Privilege escalation requires manual approval."}

此时,高风险的 bash 命令已被阻止在本仓库自动执行。

6. 在仓库中进行端到端测试

完成配置文件和脚本后,验证在本仓库使用 Copilot CLI 时钩子是否如预期运行。

验证钩子配置文件

确认你的钩子配置文件是有效的 JSON。

Bash
jq '.' < .github/hooks/copilot-cli-policy.json

检查脚本权限(类 Unix 系统)

在 macOS 和 Linux 上,确保你的 Bash 脚本具有可执行权限。

Bash
chmod +x .github/hooks/scripts/*.sh

运行基本会话

在仓库中启动一个新的 Copilot CLI 会话。

Bash
copilot -p "Show me the status of this repository"

预期结果

  • 看到来自 sessionStart 的政策横幅。
  • .github/hooks/logs/audit.jsonl 中新增一条记录(来自 userPromptSubmitted)。

触发工具使用并验证日志记录

运行会导致 Copilot CLI 调用工具的提示(例如 bash)。

Bash
copilot -p "Show me the last 5 git commits"

预期结果

  • .github/hooks/logs/audit.jsonl 中添加一条 preToolUse 条目。
  • 如果工具调用被允许,执行将正常进行。

测试被拒绝的命令

示例政策脚本包含一个临时演示规则,会阻止包含字符串 COPILOT_HOOKS_DENY_DEMO 的命令。这样可以安全验证拒绝流程,而无需运行破坏性命令。

运行会触发被拒绝命令的提示。

Bash
copilot -p "Run a test command: echo COPILOT_HOOKS_DENY_DEMO"

预期结果

  • Copilot CLI 不会执行该命令。
  • 你的钩子返回带有明确原因的拒绝响应。
  • .github/hooks/logs/audit.jsonl 中写入一条 policyDeny 条目。

确认拒绝流程正常后,请从脚本中移除演示规则,并替换为符合组织政策的拒绝模式。

检查审计日志

查看最近的条目

Bash
tail -n 50 .github/hooks/logs/audit.jsonl

仅过滤被拒绝的决策

Bash
jq 'select(.event=="policyDeny")' .github/hooks/logs/audit.jsonl

7. 安全地在团队间推广

在单个仓库验证钩子后,逐步推广以避免对开发工作流造成干扰。

选择推广策略

常见的推广方式包括:

  • 先记录后拦截的推广(推荐):先在不拒绝执行的情况下记录提示和工具使用。经过一段时间审查日志后,再引入拒绝规则。
  • 团队逐步推广:一次向一个团队或仓库部署钩子,收集反馈后再扩展到其他团队。
  • 基于风险的推广:先在处理敏感系统或生产基础设施的仓库中使用,然后再向低风险仓库扩展。

沟通期望

在强制拒绝规则之前,请确保开发者了解:

  • 钩子已在仓库中生效
  • 哪些类型的命令可能被阻止
  • 若命令被拒绝,应如何处理

明确的沟通可以减少混淆和支持请求。

保持政策可维护性

随着使用演进:

  • 在版本控制中存放钩子配置和脚本。
  • 定期审查审计日志,发现新的风险模式。
  • 逐步更新拒绝规则,而不是一次添加宽泛匹配。
  • 记录每条拒绝规则的原因,尤其是高影响的限制。

谨慎处理例外情况

某些团队(例如基础设施或平台团队)可能需要更宽松的权限。为安全处理,可:

  • 为不同仓库维护独立的钩子配置。
  • 保持例外范围狭窄并做好文档记录。
  • 避免使用破坏审计性的本地临时绕过。

延伸阅读

有关钩子故障排除,请参阅 使用 GitHub Copilot 代理的钩子

© . This site is unofficial and not affiliated with GitHub, Inc.