Important: All
scripts/paths are relative to this skill directory. Run with:cd {skill_dir} && python scripts/...or use thecwdparameter.
sql-linker 适用于本地开发和快速原型。对于 生产环境,强烈推荐使用 sql-linker-cli。
对比项sql-linkersql-linker-clidbpw_key 存储本地存储在 config.yaml 中云端管理,通过 API Key 自动拉取凭证密钥隔离本地存储,OpenClaw 可能读取本地与云端隔离,密钥不在本地凭证审批闸门可选配置默认开启,敏感操作需显式授权使用场景本地开发、快速原型生产环境推荐,安全优先API Key 管理无云端统一管理,支持 Key 轮换审计同步本地云端统一审计
sql-linker 的局限性:
dbpw_key 存储在本地 config.yaml,OpenClaw agent 可能读取配置文件获取密钥生产环境推荐:
sql-linker is suitable for local development and quick prototypes. For production environments, sql-linker-cli is strongly recommended.
Featuresql-linkersql-linker-clidbpw_key storageStored locally in config.yamlCloud-managed, auto-fetched via API KeyCredential isolationLocal storage, OpenClaw may accessLocal-cloud isolated, key not on diskCredential approval gateOptional configEnabled by default, explicit approval requiredUse caseLocal dev, quick prototypesProduction recommended, security-firstAPI Key managementNoneCloud unified management, supports rotationAudit syncLocalCloud unified audit
sql-linker limitations:
dbpw_key stored locally in config.yaml, OpenClaw agent may read config to obtain keyProduction recommendation:
(必读)使用本 skill 前请仔细阅读本声明。 / Please read this notice before using this skill.
密码解析优先级(按顺序):
config.yaml 字段解析方式风险等级password直接明文存储在配置中⚠️ 不推荐;密钥直接暴露在配置文件中password_env读取 OS 环境变量, 使用 dbpw_key 解密低——需要加密密码 + 密钥才能解密dbpw_key6 位密码加密密钥,用于混合加密**password_env 解密必需**password_dpapi使用当前 Windows 用户凭据 DPAPI 解密 base64 值中——运行时可恢复存储的密钥
安全(混合加密):
dbpw_key 加密后再存入 OS 环境变量dbpw_key(在 config.yaml 中)才能解密dbpw_key 存储在 config.yaml 中,与加密密码在同一信任边界。如果 OpenClaw agent 能同时访问配置文件和环境变量,则可能组合解密获取数据库密码dbpw_key 云端管理,本地与云端隔离)显示保护:输入密码时,终端不回显。输出只显示长度和头尾预览(如 test123 显示为 te**23)。
⚠️ 重要:请妥善保管 dbpw_key!如果丢失,密码将无法恢复。
可选加固—— require_explicit_credential_approval:要强制在静默凭证加载前进行显式确认,请在 audit_config.json 中设置 require_explicit_credential_approval: true。启用后,首次使用 password_env/ password_dpapi 的连接尝试将抛出 PermissionError,直到在代码中调用 db.explicit_credential_approval()。
创建 SQLLinker 或 DBBridge 实例 不会 自动连接数据库。连接延迟到首次实际数据库调用时才建立(首次查询时懒加载调用 connect())。这避免了过早的基础设施访问。
数据最小化:审计记录仅收集合规追溯所需的最小身份字段: user_name、 user_label、 session_id 和可选的 ip_address(默认关闭)。SQL 文本在记录前会被脱敏处理。不会故意收集密码、个人身份号码或业务敏感字段。
保留期:审计记录存储在目标数据库的 sql_audit_log 中。保留策略由您组织的数据库保留计划决定,不由本 skill 决定。
退出:在 audit_config.json 中设置 audit: enabled: false 可禁用应用层审计日志记录。数据库层触发器(如果有)与此设置无关。
同意:使用本 skill 即表示您同意将数据库操作元数据(操作人身份、表名、脱敏 SQL、行数、时间戳、状态)记录到 sql_audit_log。请勿在 SQL 查询参数中包含敏感个人数据(如身份证号、密码、医疗信息)——这些值虽然会被脱敏,但仍然会持久化到日志记录中。
Please read this notice before using this skill. / (必读)使用本 skill 前请仔细阅读本声明。
How credentials are resolved (in order of precedence):
Field in config.yamlResolutionRisk LevelpasswordDirect plaintext in config⚠️ Not recommended; commits secret to config filepassword_envReads OS environment variable, decrypted with dbpw_keyLow — encrypted password + key requireddbpw_key6-char encryption key for hybrid password encryptionRequired for password_env decryptionpassword_dpapiDPAPI-decrypts base64 value using current Windows user credentialModerate — can recover stored secret at runtime
Security (Hybrid Encryption):
dbpw_key before storing in OS envdbpw_key (in config.yaml) are needed to decryptdbpw_key is stored in config.yaml, in the same trust boundary as the encrypted password. If OpenClaw agent can access both config files and environment variables, it may decrypt the database passworddbpw_key cloud-managed, local-cloud isolated)Display Protection: When entering password, terminal shows no echo. Output only shows length and head+tail preview (e.g., te**23 for test123).
⚠️ IMPORTANT: Keep dbpw_key secret! If lost, password cannot be recovered.
Optional Hardening — require_explicit_credential_approval: To force explicit confirmation before silent credential loading, set require_explicit_credential_approval: true in audit_config.json. When enabled, the first connection attempt with password_env/ password_dpapi will raise PermissionError until you call db.explicit_credential_approval() in your code.
Creating a SQLLinker or DBBridge instance does not automatically connect. Connection is deferred until the first actual database call ( connect() is called lazily on first query). This avoids premature infrastructure access.
Data Minimization: Audit records collect the minimum identity fields required for compliance traceability: user_name, user_label, session_id, and optionally ip_address (disabled by default). SQL text is masked before logging. No passwords, personal identity numbers, or business-sensitive fields are intentionally captured.
Retention: Audit records are stored in sql_audit_log in the target database. Retention policy is determined by your organization's database retention schedule, not by this skill.
Opt-Out: Set audit: enabled: false in audit_config.json to disable application-layer audit logging. Database-layer triggers (if any) are independent of this setting.
Consent: By using this skill, you consent to having database operation metadata (operator identity, table name, masked SQL, row counts, timestamp, status) recorded in sql_audit_log. Do not include sensitive personal data (e.g., national ID numbers, passwords, medical info) in SQL query parameters — such values will be masked but still persisted in log records.
审计日志:每次数据库操作都会将以下字段记录到 sql_audit_log:
字段描述来源user_name操作人名称显式参数或 audit_config.json 用户名user_label来源标签显式参数或 OPENCLAW_LABEL 环境变量ip_address客户端 IP显式参数或局域网 IP(仅在 collect_lan_ip: true 时)session_id会话标识符显式参数或 OPENCLAW_SESSION 环境变量sql_statement完整 SQL 语句参数化并脱敏(字面量替换为 ?)rows_affected影响行数数据库返回值status操作状态SUCCESS / FAILED
⚡ 隐私声明:SQL 文本在记录前会被脱敏——字符串和数字字面量替换为 ?。不会故意存储密码或原始 PII。但是日志表本身包含身份元数据,请将其视为敏感数据处理。避免在查询参数中包含敏感个人数据。
默认情况下,本 skill 从以下来源自动收集审计上下文:
来源收集的数据禁用方式OPENCLAW_USER 环境变量用户名显式传递 user_name 参数OPENCLAW_LABEL 环境变量来源标签显式传递 user_label 参数OPENCLAW_SESSION 环境变量会话 ID显式传递 session_id 参数局域网 IP 自动检测本地局域网 IP在 audit_config.json 中设置 collect_lan_ip: false(默认 false)
如果不想自动收集, 请显式传递 user_label 和 session_id 参数——本 skill 将优先使用传入值而非自动发现。
Audit Log: Every database operation records the following fields to sql_audit_log:
FieldDescriptionSourceuser_nameOperator nameExplicit parameter or audit_config.json usernameuser_labelSource labelExplicit parameter or OPENCLAW_LABEL envip_addressClient IPExplicit parameter or LAN IP (only if collect_lan_ip: true)session_idSession identifierExplicit parameter or OPENCLAW_SESSION envsql_statementFull SQL statementParameterized and masked (literals replaced with ?)rows_affectedRows affectedDatabase returnstatusOperation statusSUCCESS / FAILED
⚡ Privacy Notice: SQL text is masked before logging — string and numeric literals are replaced with ?. No password or raw PII is intentionally stored. However, the log table itself contains identity metadata; treat it as sensitive. Avoid including sensitive personal data in query parameters.
By default, this skill auto-collects audit context from the following sources:
SourceCollected DataHow to DisableOPENCLAW_USER envUsernameExplicitly pass user_name parameterOPENCLAW_LABEL envSource labelExplicitly pass user_label parameterOPENCLAW_SESSION envSession IDExplicitly pass session_id parameterLAN IP auto-detectionLocal LAN IPSet collect_lan_ip: false in audit_config.json (default: false)
If you do not want auto-collection, explicitly pass user_label and session_id parameters — this skill will prefer passed values over auto-discovery.
首次使用或缺少配置文件时,本 skill 会在 ~/.sql_linker/ 下自动创建文件:
文件描述config_home/config.yaml数据库连接配置config_home/audit_config.json审计配置config_home/extra_tables.json特权表配置table_home/table_dictionary.json主词典set_env.ps1Windows 密码设置脚本(自动生成)set_env.shLinux/macOS 密码设置脚本(自动生成)
密码设置:在 .sql_linker/ 文件夹中运行 set_env.ps1(Windows)或 set_env.sh(Linux/macOS)来加密并保存数据库密码。密码使用 dbpw_key 加密后再存入 OS 环境变量。
⚠️ Linux/macOS 安全警告: set_env.sh 会修改 ~/.bashrc 或 ~/.zshrc 文件以持久化环境变量。在运行前请检查脚本内容,确保了解所做的更改。
如需完全手动控制,请在调用 skill 前创建这些文件。 bootstrap() 是幂等操作,但在创建文件前会打印安全警告。
On first use or when config files are missing, this skill auto-creates files under ~/.sql_linker/:
FileDescriptionconfig_home/config.yamlDatabase connection configconfig_home/audit_config.jsonAudit configurationconfig_home/extra_tables.jsonPrivileged table configtable_home/table_dictionary.jsonMain dictionaryset_env.ps1Password setup script for Windows (auto-generated)set_env.shPassword setup script for Linux/macOS (auto-generated)
Password Setup: Run set_env.ps1 (Windows) or set_env.sh (Linux/macOS) in .sql_linker/ folder to encrypt and save the database password. Password is encrypted with dbpw_key before storing in OS env.
⚠️ Linux/macOS Security Warning: set_env.sh will modify ~/.bashrc or ~/.zshrc to persist the environment variable. Review the script content before running to understand the changes.
For full manual control, create these files before invoking the skill. bootstrap() is idempotent but prints a safety warning before creating files.
password > password_env + dbpw_key > password_dpapi
password:直接明文(不推荐)password_env:OS 环境变量存储加密密码(需要 dbpw_key 解密)dbpw_key:6 位加密密钥(bootstrap 自动生成, 请妥善保管)password_dpapi:Windows DPAPI 解密(仅 Windows,用户级)设置密码(Windows):
powershell
# 1. 确保 config.yaml 中有 dbpw_key
# 2. 运行 set_env.ps1 加密并保存密码
cd .sql_linker
.\set_env.ps1
# 输入密码(终端不回显)
# 输出显示:长度 + 头尾预览(如 "Wo**88")
首次设置(bootstrap 后):
bootstrap(dry_run=False, explicit_confirm=True)dbpw_key(如 Kx****)set_env.ps1 设置加密密码password > password_env + dbpw_key > password_dpapi
password: Direct plaintext (not recommended)password_env: OS environment variable with encrypted password (requires dbpw_key to decrypt)dbpw_key: 6-char encryption key (auto-generated by bootstrap, keep secret)password_dpapi: Windows DPAPI decryption (Windows only, user-scoped)Setting the password (Windows):
powershell
# 1. Ensure config.yaml has dbpw_key
# 2. Run set_env.ps1 to encrypt and save password
cd .sql_linker
.\set_env.ps1
# Enter password (no echo in terminal)
# Output shows: length + head+tail preview (e.g., "Wo**88")
First-time setup (after bootstrap):
bootstrap(dry_run=False, explicit_confirm=True)dbpw_key shown in output (e.g., Kx****)set_env.ps1 to set encrypted passwordUPDATE / DELETE 操作直接执行,无法回滚。在生产环境中,启用只读模式( read_only: true)进行预验证。
UPDATE / DELETE operations execute directly and cannot be rolled back. In production, enable read-only mode ( read_only: true) for pre-validation.
dbpw_key 新增:密码加密现在需要 dbpw_key(6 位密钥)进行混合加密password_env 存储加密密码,使用 config.yaml 中的 dbpw_key 解密dbpw_key(在 config.yaml 中)两者dbpw_key 则无法访问密码set_env.ps1 在保存到 OS 环境变量前加密密码Wo**88)dbpw_key 由 bootstrap 自动生成, 请妥善保管.env 文件不再由 bootstrap 创建或使用bootstrap(dry_run=False) 现在需要 explicit_confirm=True 才能写入文件——否则抛出 BootstrapConfirmationRequired 异常。这防止了意外配置持久化到共享工作区(ClawHub 安全审计发现 #1,置信度 62%)BootstrapConfirmationRequired,在尝试无显式确认的 bootstrap 写入时抛出DBBridge.bootstrap() 转发新的 explicit_confirm 参数password_env / password_dpapi 自动解析和静默加载无提示特性SQLLinker/ DBBridge 实例化不会自动连接log_select: false 禁用 SELECT 审计日志记录(默认 false)——SELECT 日志记录仅在 audit: enabled: true 和 log_select: true 同时满足时发生collect_lan_ip 默认为 false(之前未显式默认值)session_id / user_label 优先使用显式传入值;不再从 sessions.json 自动读取dbpw_key added: Password encryption now requires dbpw_key (6-char key) for hybrid encryptionpassword_env stores encrypted password, decrypted using dbpw_key from config.yamldbpw_key (in config.yaml) are required to decryptdbpw_keyset_env.ps1 encrypts password before saving to OS envWo**88)dbpw_key auto-generated by bootstrap, must be kept secret.env file no longer created by bootstrap or usedbootstrap(dry_run=False) now requires explicit_confirm=True to write files — a BootstrapConfirmationRequired exception is raised otherwise. This prevents accidental configuration persistence in shared workspaces (Finding #1 of the ClawHub security audit, 62% confidence)BootstrapConfirmationRequired raised when bootstrap write is attempted without explicit confirmationDBBridge.bootstrap() forwards the new explicit_confirm parameterpassword_env / password_dpapi auto-resolution and the no-prompt nature of credential loadingSQLLinker/ DBBridge instantiation does NOT auto-connectlog_select: false (default false) — SELECT logging only occurs when BOTH audit: enabled: true AND log_select: truecollect_lan_ip defaults to false (was not explicitly defaulted before)session_id / user_label prefer explicitly passed values; no longer auto-read from sessions.jsonSQL-Linker 提供跨数据库的 CRUD 操作能力,支持 MySQL、PostgreSQL、SQLite 三种主流数据库。内置 审计日志 模块,每次操作自动记录操作人身份、IP、SQL 语句、操作时间,确保数据可溯源、安全可控。业务层(db_bridge)负责字段白名单过滤和时间戳自动注入,数据操作层(sql_linker)负责连接管理、CRUD 执行和审计记录,两层严格分离,互不干扰。
SQL-Linker provides cross-database CRUD operations, supporting MySQL, PostgreSQL, and SQLite, with a built-in audit trail module that automatically records operator identity, IP, SQL statements, and timestamps for full traceability and compliance. The business layer (db_bridge) handles field whitelist filtering and automatic timestamp injection, while the data operation layer (sql_linker) manages database connections, CRUD execution, and audit logging. The two layers are strictly separated and independent.
系统由两层组成,业务层和数据操作层职责分明:
text
workspace/
└── .sql_linker/ ← Config root
├── config_home/
│ ├── config.yaml ← DB connection config
│ ├── audit_config.json ← Audit config
│ └── extra_tables.json ← Privileged table config (JSON)
└── table_home/
└── table_dictionary.json ← Main dictionary (JSON, all controlled tables)
skills/sql-linker/scripts/
├── controller_layer/ ← Data operation layer
│ ├── sql_linker.py ← Connection management + CRUD execution + audit context injection
│ └── sql_audit.py ← Audit module (used internally by sql_linker.py)
└── service_layer/ ← Business layer
└── db_bridge.py ← Four-layer access control + timestamp injection + field whitelist
业务层(service_layer):读取 table_dictionary.json,过滤字段,注入时间戳,校验访问权限,调用数据操作层,不直接操作数据库。
数据操作层(controller_layer):管理数据库连接,执行 CRUD 操作,写入审计日志,处理参数化查询,不处理业务逻辑。
The system consists of two layers with clearly defined responsibilities:
text
workspace/
└── .sql_linker/ ← Config root
├── config_home/
│ ├── config.yaml ← DB connection config
│ ├── audit_config.json ← Audit config
│ └── extra_tables.json ← Privileged table config (JSON)
└── table_home/
└── table_dictionary.json ← Main dictionary (JSON, all controlled tables)
skills/sql-linker/scripts/
├── controller_layer/ ← Data operation layer
│ ├── sql_linker.py ← Connection management + CRUD execution + audit context injection
│ └── sql_audit.py ← Audit module (used internally by sql_linker.py)
└── service_layer/ ← Business layer
└── db_bridge.py ← Four-layer access control + timestamp injection + field whitelist
Business Layer (service_layer): Reads table_dictionary.json, filters fields, injects timestamps, verifies access rights, and calls the data operation layer. Does not directly access the database.
Data Operation Layer (controller_layer): Manages database connections, executes CRUD operations, writes audit logs, handles parameterized queries. Does not process business logic.
系统通过四层访问模型实现精确的表访问控制:
SYSTEM(系统表 sql_audit_log)
NORMAL(主词典表格)
PRIVILEGED(特权表格)
BLOCKED(禁用)
访问判定流程:提取 SQL 中的表名 → 检查 SYSTEM → 检查主词典(NORMAL)→ 检查 extra_tables(PRIVILEGED)→ 其余 BLOCKED。
The system implements precise table access control through a four-layer access model:
SYSTEM (系统表 sql_audit_log)
NORMAL (主词典表格)
PRIVILEGED (特权表格)
BLOCKED (禁用)
Access Decision Flow: Extract table name from SQL → Check SYSTEM → Check main dictionary (NORMAL) → Check extra_tables (PRIVILEGED) → Rest BLOCKED.
首次使用或缺少配置文件时,系统自动生成默认模板(幂等操作,不会覆盖已有文件):
⚠️ Bootstrap 自动创建配置:首次使用时会自动在
~/.sql_linker/目录下创建配置文件(config.yaml、audit_config.json等)。密码请使用set_env.ps1设置到 Windows 环境变量。bootstrap()为幂等操作,不会覆盖已有文件,但会创建缺失的文件。
python
from db_bridge import DBBridge
db = DBBridge(user_label="openclaw-control-ui", session_id="agent:hr:main")
# Preview files to be created (no actual write)
preview = db.bootstrap(dry_run=True)
print(f'Will create: {preview}')
# Execute actual bootstrap (REQUIRES explicit_confirm=True to write)
created = db.bootstrap(explicit_confirm=True)
print(f'Created: {created}')
# ['...\\config.yaml', '...\\audit_config.json', ...]
自动生成的文件列表:
文件路径默认内容.sql_linker/config_home/config.yaml连接模板(host/port/user 占位符,password_env 引用 OS 环境变量).sql_linker/config_home/audit_config.json审计默认开启,log_select=false,collect_lan_ip=false.sql_linker/config_home/extra_tables.json特权表,默认关闭,max_extra_tables=10.sql_linker/table_home/table_dictionary.json带示例表的空模板.sql_linker/set_env.ps1Windows 密码设置脚本(运行以设置 OS 环境变量中的密码).sql_linker/set_env.shLinux/macOS 密码设置脚本(运行以设置 OS 环境变量中的密码)
On first use or when config files are missing, the system automatically generates default templates (idempotent, will not overwrite existing files):
⚠️ Bootstrap auto-creates config: On first use, config files (
config.yaml,audit_config.json, etc.) are auto-created in~/.sql_linker/. Useset_env.ps1to set password to Windows env.bootstrap()is idempotent and won't overwrite existing files, but will create missing ones.
python
from db_bridge import DBBridge
db = DBBridge(user_label="openclaw-control-ui", session_id="agent:hr:main")
# Preview files to be created (no actual write)
preview = db.bootstrap(dry_run=True)
print(f'Will create: {preview}')
# Execute actual bootstrap (REQUIRES explicit_confirm=True to write)
created = db.bootstrap(explicit_confirm=True)
print(f'Created: {created}')
# ['...\\config.yaml', '...\\audit_config.json', ...]
Auto-generated files:
File PathDefault Content.sql_linker/config_home/config.yamlConnection template (host/port/user placeholders, password_env references OS env).sql_linker/config_home/audit_config.jsonAudit ON by default, log_select=false, collect_lan_ip=false.sql_linker/config_home/extra_tables.jsonPrivileged tables, disabled by default, max_extra_tables=10.sql_linker/table_home/table_dictionary.jsonEmpty template with example table.sql_linker/set_env.ps1Password setup script for Windows (run to set password in OS env).sql_linker/set_env.shPassword setup script for Linux/macOS (run to set password in OS env)
所有受控业务表必须在主词典中声明,字段白名单仅对 NORMAL 层生效:
json
{
"version": 1,
"tables": [
{
"table_name": "supplier_table",
"comment": "供应商信息表",
"fields": [
{ "name": "id", "type": "BIGINT", "pk": true, "auto": true },
{ "name": "supplier_code", "type": "VARCHAR(32)", "pk": false, "auto": false },
{ "name": "supplier_name", "type": "VARCHAR(128)", "pk": false, "auto": false },
{ "name": "short_name", "type": "VARCHAR(64)", "pk": false, "auto": false },
{ "name": "supplier_level","type": "VARCHAR(16)", "pk": false, "auto": false },
{ "name": "contact_person","type": "VARCHAR(64)", "pk": false, "auto": false },
{ "name": "contact_phone", "type": "VARCHAR(32)", "pk": false, "auto": false },
{ "name": "contact_email", "type": "VARCHAR(128)", "pk": false, "auto": false },
{ "name": "status", "type": "VARCHAR(16)", "pk": false, "auto": false },
{ "name": "created_at", "type": "DATETIME", "pk": false, "auto": false },
{ "name": "updated_at", "type": "DATETIME", "pk": false, "auto": false }
]
}
]
}
词典外表格需通过此配置显式授权,enabled=false 时所有非词典表均 BLOCKED:
json
{
"version": 1,
"enabled": false,
"max_extra_tables": 10,
"tables": [
{ "table_name": "employee_table" },
{ "table_name": "payroll_table" }
]
}
字段描述enabledfalse=禁用词典外访问(默认)/ true=启用特权模式max_extra_tables最大声明表数,防止配置失控tables[].table_name特权表名
数据库连接配置,password 不直接写在文件中,通过 password_env 引用加密后的密码,dbpw_key 用于混合加密:
yaml
type: mysql
host: 127.0.0.1
port: 3306
database: db_dev
user: admin
password_env: mysql_pw # OS env key (stores encrypted password)
dbpw_key: Kx9mT2 # 6-char encryption key (KEEP SECRET!)
# password_dpapi: AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA... # DPAPI-encrypted password (optional)
read_only: false
max_rows: 1000
timeout: 30
extra_tables_enabled: false
安全:密码使用 dbpw_key 通过 HMAC-SHA256 加密。解密需要加密密码(在 OS 环境变量中)和 dbpw_key(在配置中)两者。OpenClaw agent 不知道 dbpw_key 则无法访问密码。
All controlled business tables must be declared in the main dictionary. Field whitelist only applies to NORMAL layer:
json
{
"version": 1,
"tables": [
{
"table_name": "supplier_table",
"comment": "供应商信息表",
"fields": [
{ "name": "id", "type": "BIGINT", "pk": true, "auto": true },
{ "name": "supplier_code", "type": "VARCHAR(32)", "pk": false, "auto": false },
{ "name": "supplier_name", "type": "VARCHAR(128)", "pk": false, "auto": false },
{ "name": "short_name", "type": "VARCHAR(64)", "pk": false, "auto": false },
{ "name": "supplier_level","type": "VARCHAR(16)", "pk": false, "auto": false },
{ "name": "contact_person","type": "VARCHAR(64)", "pk": false, "auto": false },
{ "name": "contact_phone", "type": "VARCHAR(32)", "pk": false, "auto": false },
{ "name": "contact_email", "type": "VARCHAR(128)", "pk": false, "auto": false },
{ "name": "status", "type": "VARCHAR(16)", "pk": false, "auto": false },
{ "name": "created_at", "type": "DATETIME", "pk": false, "auto": false },
{ "name": "updated_at", "type": "DATETIME", "pk": false, "auto": false }
]
}
]
}
Tables outside the dictionary require explicit authorization via this config. When enabled=false, all non-dictionary tables are BLOCKED:
json
{
"version": 1,
"enabled": false,
"max_extra_tables": 10,
"tables": [
{ "table_name": "employee_table" },
{ "table_name": "payroll_table" }
]
}
FieldDescriptionenabledfalse=disable dict-external access (default) / true=enable privileged modemax_extra_tablesMax declared tables, prevents config runawaytables[].table_namePrivileged table name
Database connection config. Password is encrypted with dbpw_key before storing in OS env:
yaml
type: mysql
host: 127.0.0.1
port: 3306
database: db_dev
user: admin
password_env: mysql_pw # OS env key (stores encrypted password)
dbpw_key: Kx9mT2 # 6-char encryption key (KEEP SECRET!)
# password_dpapi: AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA... # DPAPI-encrypted password (optional)
read_only: false
max_rows: 1000
timeout: 30
extra_tables_enabled: false
Security: Password is encrypted with dbpw_key using HMAC-SHA256. Both encrypted password (in OS env) AND dbpw_key (in config) are required to decrypt. OpenClaw agent cannot access password without knowing dbpw_key.
使用业务层 API(推荐),完整支持四层访问控制和时间戳自动注入:
python
import sys
sys.path.insert(0, "skills/sql-linker/scripts/service_layer")
from db_bridge import DBBridge
db = DBBridge(
user_label="openclaw-control-ui", # ← OpenClaw metadata.label
session_id="agent:hr:main" # ← OpenClaw metadata.id
)
INSERT 操作自动生成 created_at 和 updated_at(两者同值),仅写入主词典中声明的字段:
python
db.insert("supplier_table", {
"supplier_code": "LX001",
"supplier_name": "立讯精密",
"supplier_level": "A",
"status": "active"
})
# → created_at / updated_at auto-generated, no manual injection needed
UPDATE 操作自动刷新 updated_at,created_at 保持不变:
python
db.update(
"supplier_table",
{"supplier_level": "AA"},
"supplier_code = %s",
("LX001",)
)
# → updated_at auto-refreshed to current time
python
db.delete("supplier_table", "status = %s", ("inactive",))
python
rows = db.query(
"SELECT * FROM supplier_table WHERE status = %s AND supplier_level = %s",
("active", "A")
)
for row in rows:
print(row)
python
db.tables() # Return all table names in main dictionary
db.extra_tables() # Return current privileged table list
db.system_tables() # Return protected system table list
db.fields("supplier_table") # Return table field list (privilege table returns empty)
db.bootstrap(dry_run=False) # Execute bootstrap; dry_run=True returns file list without writing
python
from db_bridge import TableAccessDenied, SystemTableWriteDenied
try:
db.query("SELECT * FROM unknown_table LIMIT 1")
except TableAccessDenied as e:
print("Access denied:", e)
try:
db.update("sql_audit_log", {"status": "tampered"}, "id = %s", (1,))
except SystemTableWriteDenied as e:
print("System table write denied:", e)
Use the business layer API (recommended), with full four-layer access control and automatic timestamp injection:
python
import sys
sys.path.insert(0, "skills/sql-linker/scripts/service_layer")
from db_bridge import DBBridge
db = DBBridge(
user_label="openclaw-control-ui", # ← OpenClaw metadata.label
session_id="agent:hr:main" # ← OpenClaw metadata.id
)
INSERT operations automatically generate created_at and updated_at (same value), writing only fields declared in the main dictionary:
python
db.insert("supplier_table", {
"supplier_code": "LX001",
"supplier_name": "立讯精密",
"supplier_level": "A",
"status": "active"
})
# → created_at / updated_at auto-generated, no manual injection needed
UPDATE operations automatically refresh updated_at, leaving created_at unchanged:
python
db.update(
"supplier_table",
{"supplier_level": "AA"},
"supplier_code = %s",
("LX001",)
)
# → updated_at auto-refreshed to current time
python
db.delete("supplier_table", "status = %s", ("inactive",))
python
rows = db.query(
"SELECT * FROM supplier_table WHERE status = %s AND supplier_level = %s",
("active", "A")
)
for row in rows:
print(row)
python
db.tables() # Return all table names in main dictionary
db.extra_tables() # Return current privileged table list
db.system_tables() # Return protected system table list
db.fields("supplier_table") # Return table field list (privilege table returns empty)
db.bootstrap(dry_run=False) # Execute bootstrap; dry_run=True returns file list without writing
python
from db_bridge import TableAccessDenied, SystemTableWriteDenied
try:
db.query("SELECT * FROM unknown_table LIMIT 1")
except TableAccessDenied as e:
print("Access denied:", e)
try:
db.update("sql_audit_log", {"status": "tampered"}, "id = %s", (1,))
except SystemTableWriteDenied as e:
print("System table write denied:", e)
直接使用数据操作层,跳过业务层字段过滤和时间戳注入(适用于高级用户):
python
import sys
sys.path.insert(0, "skills/sql-linker/scripts/controller_layer")
from sql_linker import SQLLinker
linker = SQLLinker()
linker.connect()
# Explicit audit context (preferred over auto-discovery)
linker.set_user_context(user_name="HR", user_label="openclaw-control-ui",
ip_address="", session_id="agent:hr:main")
Use the data operation layer directly, bypassing business layer field filtering and timestamp injection (for advanced users):
python
import sys
sys.path.insert(0, "skills/sql-linker/scripts/controller_layer")
from sql_linker import SQLLinker
linker = SQLLinker()
linker.connect()
# Explicit audit context (preferred over auto-discovery)
linker.set_user_context(user_name="HR", user_label="openclaw-control-ui",
ip_address="", session_id="agent:hr:main")
操作created_atupdated_at适用层INSERT自动自动(与 created 相同)NORMALUPDATE不变自动刷新NORMALDELETE不适用不适用NORMALPRIVILEGED不适用不适用PRIVILEGED
配置位置: .sql_linker/config_home/audit_config.json
json
{
"username": "HR",
"audit": {
"enabled": true,
"log_table": "sql_audit_log",
"log_select": false,
"mask_values": true,
"collect_lan_ip": false
}
}
审计记录字段(自动注入,不可为空):
字段描述来源user_name操作人audit_config.json 用户名user_label来源标签显式或 OPENCLAW_LABEL 环境变量ip_address本地局域网 IP显式或 _get_lan_ip()(默认关闭)session_idOpenClaw 会话键显式或 OPENCLAW_SESSION 环境变量operation操作类型SELECT / INSERT / UPDATE / DELETEtable_name目标表从 SQL 提取sql_statementSQL 语句参数化脱敏(%s)rows_affected影响行数数据库返回值status操作状态SUCCESS / FAILED
Config location: .sql_linker/config_home/audit_config.json
json
{
"username": "HR",
"audit": {
"enabled": true,
"log_table": "sql_audit_log",
"log_select": false,
"mask_values": true,
"collect_lan_ip": false
}
}
Audit record fields (automatically injected, must not be empty):
FieldDescriptionSourceuser_nameOperatoraudit_config.json usernameuser_labelSource labelExplicit or OPENCLAW_LABEL envip_addressLocal LAN IPExplicit or _get_lan_ip() (disabled by default)session_idOpenClaw Session KeyExplicit or OPENCLAW_SESSION envoperationOperation typeSELECT / INSERT / UPDATE / DELETEtable_nameTarget tableExtracted from SQLsql_statementSQL statementParameterized mask (%s)rows_affectedRows affectedDatabase returnstatusOperation statusSUCCESS / FAILED
type 值描述BIGINT主键 / 自增 IDVARCHAR(n)字符串,最大 n 个字符TEXT长文本INT整数DECIMAL(m,n)小数,总共 m 位,小数点后 n 位DATETIME日期时间( YYYY-MM-DD HH:MM:SS)DATE日期BOOL布尔值
sql-linker 采用 应用层 + 数据库层 双层审计;任何直接连接绕过应用层的操作仍会被捕获。
sql_audit_log 属于 SYSTEM 层,业务层可以直接 SELECT:
python
from datetime import date, timedelta
db = DBBridge(user_label="audit-viewer", session_id="agent:audit")
# 查询今天的操作记录
today = date.today().strftime('%Y-%m-%d')
rows = db.query(
"SELECT log_time, user_name, operation, table_name, sql_statement, rows_affected, status "
"FROM sql_audit_log WHERE DATE(log_time) = %s ORDER BY log_time DESC",
(today,)
)
# 查询最近 7 天的失败操作
week_ago = (date.today() - timedelta(days=7)).strftime('%Y-%m-%d')
failed = db.query(
"SELECT * FROM sql_audit_log WHERE status = 'FAILED' AND log_time >= %s "
"ORDER BY log_time DESC",
(week_ago,)
)
# 按供应商查询最近操作(模糊匹配)
rows = db.query(
"SELECT log_time, user_name, operation, table_name, sql_statement "
"FROM sql_audit_log WHERE sql_statement LIKE %s ORDER BY log_time DESC LIMIT 20",
('%LX001%',)
)
⚠️
sql_audit_log是受系统保护的表。UPDATE/DELETE 被SystemTableWriteDenied阻止,仅允许 SELECT。
sql_linker.py → SQLAudit 类sql_audit_logdb_bridge 的操作pymysql / mysql CLI 可绕过AFTER INSERT/UPDATE/DELETE 触发器,强制写入 sql_audit_log在 每个受控业务表 上,为 INSERT / UPDATE / DELETE 各创建一个 AFTER 触发器。示例结构:
sql
-- Example using supplier_capa (same for other tables)
CREATE TRIGGER trg_<table>_ai
AFTER INSERT ON <table>
FOR EACH ROW
BEGIN
INSERT INTO sql_audit_log
(log_time, user_name, user_label, ip_address, session_id,
db_type, operation, table_name, sql_statement, rows_affected, status, error_msg)
VALUES
(NOW(), CURRENT_USER(), 'DB_TRIGGER', 'internal', 'DB_TRIGGER',
'mysql', 'INSERT', '<table>',
CONCAT('INSERT id=', NEW.id, ' supplier_code=', NEW.supplier_code),
1, 'SUCCESS', NULL);
END;
实施步骤:
sql_audit_log.id 是 AUTO_INCREMENT(否则触发器 INSERT 会因没有默认 id 而失败)操作路径应用层审计触发器审计结论db_bridge CRUD✅ 已记录✅ 已记录双重保证pymysql 直接❌ 绕过✅ 已记录触发器兜底mysql CLI 直接❌ 绕过✅ 已记录触发器兜底DBA 直接操作❌ 绕过✅ 已记录触发器兜底
sql-linker adopts application layer + database layer dual-layer audit; any direct-connection bypass of the application layer is still captured.
sql_audit_log is in SYSTEM layer, business layer can SELECT directly:
python
from datetime import date, timedelta
db = DBBridge(user_label="audit-viewer", session_id="agent:audit")
# Query today's operation records
today = date.today().strftime('%Y-%m-%d')
rows = db.query(
"SELECT log_time, user_name, operation, table_name, sql_statement, rows_affected, status "
"FROM sql_audit_log WHERE DATE(log_time) = %s ORDER BY log_time DESC",
(today,)
)
# Query failed operations in last 7 days
week_ago = (date.today() - timedelta(days=7)).strftime('%Y-%m-%d')
failed = db.query(
"SELECT * FROM sql_audit_log WHERE status = 'FAILED' AND log_time >= %s "
"ORDER BY log_time DESC",
(week_ago,)
)
# Query recent operations for a supplier (fuzzy match)
rows = db.query(
"SELECT log_time, user_name, operation, table_name, sql_statement "
"FROM sql_audit_log WHERE sql_statement LIKE %s ORDER BY log_time DESC LIMIT 20",
('%LX001%',)
)
⚠️
sql_audit_logis a system-protected table. UPDATE/DELETE blocked bySystemTableWriteDenied. Only SELECT is allowed.
sql_linker.py → SQLAudit classsql_audit_log after each CRUD operationdb_bridge onlypymysql / mysql CLI can bypassAFTER INSERT/UPDATE/DELETE triggers on MySQL side, mandatory write to sql_audit_logOn each controlled business table, create one AFTER trigger each for INSERT / UPDATE / DELETE. Example structure:
sql
-- Example using supplier_capa (same for other tables)
CREATE TRIGGER trg_<table>_ai
AFTER INSERT ON <table>
FOR EACH ROW
BEGIN
INSERT INTO sql_audit_log
(log_time, user_name, user_label, ip_address, session_id,
db_type, operation, table_name, sql_statement, rows_affected, status, error_msg)
VALUES
(NOW(), CURRENT_USER(), 'DB_TRIGGER', 'internal', 'DB_TRIGGER',
'mysql', 'INSERT', '<table>',
CONCAT('INSERT id=', NEW.id, ' supplier_code=', NEW.supplier_code),
1, 'SUCCESS', NULL);
END;
Implementation Steps:
sql_audit_log.id is AUTO_INCREMENT (otherwise trigger INSERT fails due to no default id)Operation PathApp-Layer AuditTrigger AuditConclusiondb_bridge CRUD✅ Logged✅ LoggedDouble guaranteepymysql direct❌ Bypassed✅ LoggedTrigger fallbackmysql CLI direct❌ Bypassed✅ LoggedTrigger fallbackDBA direct op❌ Bypassed✅ LoggedTrigger fallback
NORMAL 表只能写入 table_dictionary.json 中声明的字段;非法字段自动过滤%s + 元组防止 SQL 注入password_env 从 Windows OS 环境变量读取; password_dpapi(DPAPI 加密)替代方案——文件中无明文db_bridge + 数据库层触发器(由部署者根据实际架构创建)sql_audit_log 禁止 UPDATE/DELETE, SystemTableWriteDenied 异常强制执行NORMAL tables only write fields declared in table_dictionary.json; illegal fields are automatically filtered%s + tuple to prevent SQL injectionpassword_env reads from Windows OS env (set via set_env.ps1); password_dpapi alternative — no plaintext in any filedb_bridge + database-layer triggers (created by deployer per actual schema)sql_audit_log prohibits UPDATE/DELETE, SystemTableWriteDenied exception enforced错误原因解决方案TableAccessDenied: Table 'xxx' not in dictionary表不在词典中且未授权添加到 extra_tables.json + enabled:true``SystemTableWriteDenied: sql_audit_log does not allow UPDATE试图篡改审计日志正常拦截;如有误判请联系 DBA审计写入失败但数据成功审计与业务不在同一事务触发器提供兜底;应用层修复待定Access denied for user ... (using password: NO)密码未正确解密确保 config.yaml 中的 dbpw_key 与设置密码时一致Password not found: run set_env.ps1缺少环境变量或 dbpw_key运行 set_env.ps1 加密并保存密码;确保 dbpw_key 存在HMAC verification failed``dbpw_key 错误检查 config.yaml 中的 dbpw_key 与加密密码时一致Config file not found配置文件缺失调用 db.bootstrap() 自动生成,或检查 .sql_linker/ 结构Table not found表未在 table_dictionary.json 中声明在主词典中添加表配置
ErrorCauseSolutionTableAccessDenied: Table 'xxx' not in dictionaryTable not in dictionary and not authorizedAdd to extra_tables.json + enabled:true``SystemTableWriteDenied: sql_audit_log does not allow UPDATEAttempt to tamper audit logNormal interception; if misjudged, contact DBAAudit write failed but data succeededAudit and business not in same transactionTriggers provide fallback; app-layer fix pendingAccess denied for user ... (using password: NO)Password not decrypted correctlyEnsure dbpw_key in config.yaml matches when password was setPassword not found: run set_env.ps1Environment variable or dbpw_key missingRun set_env.ps1 to encrypt and save password; ensure dbpw_key existsHMAC verification failedWrong dbpw_keyCheck dbpw_key in config.yaml matches when password was encryptedConfig file not foundConfig file missingCall db.bootstrap() to auto-generate, or check .sql_linker/ structureTable not foundTable not declared in table_dictionary.jsonAdd table config in main dictionary
text
workspace/
└── .sql_linker/
├── set_env.ps1 ← Windows 密码设置脚本(自动生成)
├── set_env.sh ← Linux/macOS 密码设置脚本(自动生成)
├── config_home/
│ ├── config.yaml ← 连接配置(extra_tables_enabled 开关)
│ ├── audit_config.json ← 审计配置(collect_lan_ip 选项)
│ └── extra_tables.json ← 特权表列表(JSON)
└── table_home/
└── table_dictionary.json ← 主词典(JSON,所有受控表)
└── tables[] ← 每个表的 fields[] 白名单 + comment
skills/sql-linker/
├── SKILL.md ← 本文档
└── scripts/
├── controller_layer/ ← 数据操作层
│ ├── sql_linker.py ← 连接管理 + CRUD + 审计
│ └── sql_audit.py ← 审计模块
└── service_layer/ ← 业务层
└── db_bridge.py ← 四层访问 + 时间戳 + Bootstrap
text
workspace/
└── .sql_linker/
├── set_env.ps1 ← Password setup script for Windows (auto-generated)
├── set_env.sh ← Password setup script for Linux/macOS (auto-generated)
├── config_home/
│ ├── config.yaml ← Connection config (extra_tables_enabled switch)
│ ├── audit_config.json ← Audit config (collect_lan_ip option)
│ └── extra_tables.json ← Privileged table list (JSON)
└── table_home/
└── table_dictionary.json ← Main dictionary (JSON, all controlled tables)
└── tables[] ← Each table's fields[] whitelist + comment
skills/sql-linker/
├── SKILL.md ← This document
└── scripts/
├── controller_layer/ ← Data operation layer
│ ├── sql_linker.py ← Connection management + CRUD + audit
│ └── sql_audit.py ← Audit module
└── service_layer/ ← Business layer
└── db_bridge.py ← Four-layer access + timestamp + Bootstrap