首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >我给 AI 开了「完全访问」权限,回头查了它到底干了什么 #WorkBuddy#

我给 AI 开了「完全访问」权限,回头查了它到底干了什么 #WorkBuddy#

原创
作者头像
真的就是初学者
发布于 2026-09-08 10:09:45
发布于 2026-09-08 10:09:45
2910
举报

我机器上挂着十几个定时任务。前几天我翻设置,发现其中 5 个的权限一栏写着「完全允许访问」。

这五个字让我愣了一下。意思是:这几个任务每天定点跑的时候,不用问我,想读就读、想写就写、想跑命令就跑命令。而我,从开权限那天起,从来没逐条看过它们到底干了什么。

那天上午我把这 5 个任务审了一遍。不是让它自己汇报——AI 说自己「只做了 A、B、C」不算数,得看它留下的原始记录。这篇就把这套审计方法写下来,脚本不到一百行,不需要装任何东西,你也能照着跑一遍。

一、先查这个权限是什么时候开的

在客户端任务编辑面板的「权限」项,能看到这几个任务是 fullAccess。我想知道它是什么时候变成这样的。

每个自动化在 C:\Users\你的用户名\.workbuddy\automation-backups\ 下面都有一个 json 备份,重点看两个字段:createdAt 和 updatedAt。我这几个任务,两个时间完全相等,而 permissionMode 已经是 fullAccess——说明建任务那一刻就带着这个权限,不是后来被谁改的,也不是被别的程序动了手脚。

为什么建的时候就给?因为定时任务是无人值守的。如果设成「每步询问」,半夜十一点触发了没人点确认,任务就卡死或者失败。所以建的时候默认就给了完全权限。

这个结论挺重要:问题不在「有人偷偷改了我的设置」,在于我从开权限那天起就没审过它。

二、审计思路:不看它的自述,看它留下的记录

AI 每次跑完会给你一段总结,但那是它自己写的,可信度有限。要查,就查两处原始记录:

  • 任务与运行记录——一个 sqlite 数据库(客户端的 workbuddy.db,主要用 automations 和 automation_runs 两张表),里面记着每个任务跑过哪几次、每次对应哪个会话。
  • 会话原始记录——C:\Users\你的用户名\.workbuddy\projects\ 下面层层目录里的 .jsonl 文件,一行一个 JSON,每一次工具调用和参数原样写死在里面,改不了也编不了。

思路就一句话:先从数据库拿到「这个任务跑过哪些会话 id」,再拿这些 id 去 jsonl 里把每一次工具调用捞出来,然后分类统计。

三、第一步:从数据库捞出会话 id

import sqlite3, json

DB = r'C:\Users\你的用户名\.workbuddy\workbuddy.db'

IDS = ['automation-1788142020002', 'automation-1788142215361'] # 审几个填几个

con = sqlite3.connect(DB)

NAMES = dict(con.execute("SELECT id,name FROM automations"))

conv = {}

for aid in IDS:

    for (rj,) in con.execute("SELECT runs_json FROM automation_runs WHERE automation_id=?", (aid,)):

        try:

            arr = json.loads(rj or '[]')

        except Exception:

            arr = []

        for it in arr:

            if it.get('conversationId'):

                conv[it['conversationId']] = NAMES.get(aid)

print('任务数', len(IDS), '会话数', len(conv))

要点:automations 表存任务名,automation_runs 表里的 runs_json 是个 JSON 数组,每次运行一条,里面有 conversationId。这个 id 就是磁盘上 jsonl 文件的文件名。数据库只读打开,一个字节都不动它。

任务 id 怎么找?automations 表全查一遍,筛 permissionMode 字段是 fullAccess 的即可。

四、第二步:从原始记录还原每一次工具调用

import glob, os, json, collections

PROJ = r'C:\Users\你的用户名\.workbuddy\projects'

files = glob.glob(os.path.join(PROJ, '**', '*.jsonl'), recursive=True)

hit = {}

for p in files:

    cid = os.path.basename(p).split('.')[0]

    if cid in conv:

        hit[cid] = p

tools = collections.Counter()

calls = []

for cid, p in hit.items():

    with open(p, encoding='utf-8', errors='ignore') as f:

        for ln in f:

            try:

                o = json.loads(ln)

            except Exception:

                continue

            if o.get('type') != 'function_call':

                continue

            nm = o.get('name')

            args = o.get('arguments')

            if isinstance(args, str):

                try:

                    args = json.loads(args)

                except Exception:

                    args = {'_raw': args}

            tools[nm] += 1

            calls.append((conv[cid], nm, args if isinstance(args, dict) else {}))

for t, n in tools.most_common():

    print(' {:24s} {}'.format(str(t), n))

这里有两个坑:一是 arguments 有时候是 JSON 字符串、有时候已经是对象,两种都得接住;二是单行解析失败就跳过,别让整个脚本挂掉——几万行里混几行畸形很正常。

五、第三步:四类检查

拿到 calls 这个列表之后,做四件事:

  • 危险关键词扫描:把参数整体转小写,跟一张词表比。
  • 写操作落在哪个目录:所有 Write / Edit / MultiEdit 的 file_path 取目录名计数。
  • 跑过哪些命令:Bash 的 command 字段去重打出来。
  • 访问过哪些网址:WebFetch / WebSearch 的 url 和 query 汇总。

RISK = ['rm -rf', 'rmdir', 'del /f', 'del /s', 'format ', 'remove-item', 'shutdown',

        'reg delete', 'taskkill', 'invoke-webrequest', 'curl ', 'wget ', 'netsh',

        'schtasks', 'takeown', 'icacls', 'git push', 'send-mail', 'agent_mail']

WS = 'd:\\workbuddy\\你的项目名' # 换成你自己的工作区,必须小写

bad = []

dirs = collections.Counter()

out_files = []

for a, n, v in calls:

    s = json.dumps(v, ensure_ascii=False).lower()

    if any(k in s for k in RISK):

        bad.append((a, n, s[:150]))

    if n in ('Write', 'Edit', 'MultiEdit', 'NotebookEdit'):

        fp = str(v.get('file_path') or v.get('path') or '')

        if fp:

            dirs[os.path.dirname(fp).lower()] += 1

            if not fp.lower().startswith(WS):

                out_files.append((a, fp))

print('危险命中', len(bad))

print('工作区外写入', len(out_files))

注意路径比较一定要统一转小写。Windows 上盘符和目录名大小写不一致,不转小写会漏判,本来越界的写操作会被当成安全。

危险词表不用写得多,覆盖这几类就够:删文件、格式化、改注册表、建计划任务、杀进程、关机、联网下载、往外发数据、调邮箱工具。

六、跑出来是什么结果

我本机跑完的输出长这样(节选):

被审会话 23 个,工具调用 591 次

=== 危险关键词扫描:命中 1 条 ===

  [签到补漏] Bash :: {"command": "curl http://127.0.0.1:11434/api/tags"}

=== 写操作目录 ===

  [工作区内] d:\workbuddy\你的项目名 ×203

  [区外] c:\users\你的用户名\.workbuddy ×5

逐条看下来:

  • 二十多个会话、近六百次工具调用,运行目录全部落在工作区内,一次都没跑出去。
  • 删除、格式化、改注册表、建计划任务、杀进程:零次。
  • 发邮件、上传、往外发数据:零次。
  • 唯一命中的那条「危险」是 curl 本机 11434 端口——那是我自己装的本地模型服务,安全。
  • 工作区外那 5 条写入,全部落在它自己的记忆文件和技能目录里,属于自我维护,不是往外传。
  • 联网一共三次搜索,没有一次数据外发。

看到这个结果我松了口气。但心里也清楚:这只能说明过去没出事,不等于以后不会出事。

七、真正的敞口不在定时任务本身

审计过程里翻到两件更值得注意的事。

第一件:自动化线程被当成普通聊天窗口续用了。有个签到任务的线程,后面接着聊了别的事,还调了技能管理、连了资料库。这些线程同样带着完全权限——在里面随口说一句,也是「不问就干」。风险不在定时任务本身,在这类续用窗口缺少二次确认。

第二件:有任务失败过,但原因不是权限。报错是「会话在任务完成前结束」,那是免费模型被限流掐断,跟权限没关系,别往安全上瞎怀疑。

所以结论很明确:权限不是问题,提示词的边界才是问题。

八、我做的三件加固

第一件:权限维持现状,不动。

降成「每次询问」听起来更安全,但定时触发时没人确认,任务直接跑不完——拿安全性换来的可能是任务全废。这笔账不划算。

第二件:给 5 个任务的提示词各加一段「安全硬约束」,并声明最高优先级(与提示词其他内容冲突时,以本节为准):

  • 只在工作区内读写,越界一律不执行并如实报告(例外:自身记忆文件、技能目录,且仅限文本文件)
  • 禁止删除和一切破坏性操作
  • 禁止改系统:注册表、计划任务、环境变量、杀进程、关机重启
  • 禁止外发数据;允许只读探测本机服务、只读搜索
  • 只运行工作区内既有脚本,不下载执行来路不明的程序
  • 需要靠被禁操作才能完成时立刻停下,不做变通绕过
  • 发文那个任务单独再加一条:禁止代替我点发布

加完回数据库核验一遍:5 条记录的提示词里都有这段,状态和触发时间一点没变。改提示词不影响自动执行,代价几乎为零,但边界清楚了。

第三件:改掉一个习惯——不在自动化任务的线程里接着聊别的活儿,要聊就新开一条对话。

顺带提醒一句:改权限的正规途径是客户端任务编辑面板的「权限」项。自动化管理接口改不了这个字段,直接改数据库有被客户端缓存覆盖的风险,别干这种事。

九、小结

这回审计给我最大的触动不是「AI 干了什么坏事」——它一件都没干。触动在于:我给出去一个很高的权限,然后大半年没回头看过一眼。安全这件事,最怕的不是别人动手,是自己忘了。

脚本我已经留在本地,隔段时间重跑一次,几十秒出结果。你如果手上也有挂着「完全访问」的定时任务,建议照着这套跑一遍。查完心里有底,比什么都强。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先查这个权限是什么时候开的
  • 二、审计思路:不看它的自述,看它留下的记录
  • 三、第一步:从数据库捞出会话 id
  • 四、第二步:从原始记录还原每一次工具调用
  • 五、第三步:四类检查
  • 六、跑出来是什么结果
  • 七、真正的敞口不在定时任务本身
  • 八、我做的三件加固
  • 九、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档