grok

package
v0.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

acp.go —— ACP(Agent Client Protocol)的 WebSocket JSON-RPC 双向客户端。

职责:

  • 维护一条到 grok agent serve 的 WS 连接,跑 JSON-RPC 2.0 双向消息
  • 我方请求(initialize/session.*)按 id 匹配响应;session/prompt 用 CallAsync 异步等待(它要跑完一整个回合才响应)
  • 对方通知(session/update 及 _x.ai/* 私有通知)经 OnNotify 分发
  • 对方请求(session/request_permission)经 OnPermission 上抛,应答可延迟 任意久后经 Reply 回发——协调者可能过夜才裁决(spec §5.1 实测 20min 无超时)

边界:

  • 不认识 ACP 的业务语义(不知道什么是权限、什么是回合),只做协议管道; 语义翻译在 adapter.go
  • 不重连:重连策略属 adapter 的生命周期决策,本层只在连接死亡时 OnClosed 通知

adapter.go —— ACP 语义到 executor.Adapter 契约的翻译层。

职责:

  • 把 StartServe / DialACP / initialize / session.new / session.prompt 编排成 Adapter 的五动作
  • ACP 消息 → AdapterEvent 映射:session/request_permission → permission 事件; agent_message_chunk 累积成回合正文(thought 与 tool_call 只进 render.log); session/prompt 的响应(stopReason)作回合边界 → turn.ParseTrailer 分类
  • 可见性:回合文本增量追加到 <taskDir>/render.log,供 handoff attach 旁观

边界:

  • 不写 store、不做审批判断(见 executor.go 包级边界):会话 id 等持久化诉求 经事件(progress「会话就绪」/ Result.SessionID)交 manager 落库
  • 不做任务状态机迁移:6 状态迁移完全由 manager 负责

与 opencode adapter 的两处结构性差异:

  • 回合边界是 session/prompt 的**响应**而非从 idle 事件推断,因此不需要 opencode 的 idleGrace 去抖与 scheduleIdle/resolveIdle/cancelPendingIdle 竞态处理
  • 权限是阻塞式 JSON-RPC 请求,需维护 permID → 请求 id 的挂起表(见 perm.go)

authsync.go —— grok 任务级 home 里凭据副本的收编写回。

职责:

  • 发现 <taskDir>/grokhome/auth.json 从软链变成了普通文件(grok 刚在这里刷新过)
  • 逐账号键比 expires_at,把严格更新的条目收编进权威副本 ~/.grok/auth.json
  • 收尾把任务侧恢复成软链,复位「权威副本只有一份」的不变量

边界:

  • 不起 goroutine:调用点是已有的 per-task 看门狗(resume.go 的 watchdog)
  • 不解析条目内部结构:条目按 json.RawMessage 整体搬运,只读 expires_at 一个 字段用于比较——grok 升级改字段名不会让我们把用户凭据写残
  • 不负责首次建链:那是 EnsureAuthLink 的职责,本文件只在发现破链时复位

为什么是「允许出现副本、及时收编」而不是「禁止出现副本」:grok 刷新令牌时替换 的是目录项(rename 或 unlink+create),软链和硬链都拦不住,禁止不了。详见 docs/superpowers/specs/2026-08-09-handoff-grok-credential-ownership-design.md §2。

日志纪律:只打账号键、expires_at、任务 id,任何情况下不打 token 值。

perm.go —— ACP 权限门:挂起表、裁决回发与连接断开时的处置。

职责:

  • 暂存 toolCallId → ACP 请求 id,等 manager 的裁决回来后经 Reply 回发
  • 把 handoff 的 once/reject 翻译为 ACP 的 allow-once/reject-once
  • 连接断开时作废全部挂起项并告知调用方

边界:

  • 不做审批判断:批不批由 manager 依协调者/审批者的应答决定,本层只转发
  • 不碰 store:工单、黑名单、升级链全在 manager

为什么需要挂起表(opencode 没有):ACP 的权限是 agent→client 的**阻塞式 JSON-RPC 请求**,应答必须带原请求 id 回发;而 opencode 的权限应答是一次 独立的 HTTP POST,无需保留连接级状态。

为什么断开即作废且不再尝试救回:spike 实测 WS 断开后重连 + session/load 成功,但未决的权限请求**不会被重发**,grok 侧那次工具调用永久卡在等应答。 此时假装恢复成功比直接失败更危险——任务会静止而无人知晓。

probe.go —— 只读存活探测。

职责:

  • Probe:读 proc.json,走 Proc.Alive 的既有判据(存活锁 + HTTP 应答), 如实返回存活结论

边界:

  • **绝不写**:不回收执行者进程、不动凭据软链、不碰 store、不发事件
  • 不打印 Proc.Secret / WSURL:两者都含 secret,绝不进日志

proc.go —— grok agent serve 的进程生命周期:prochost 托管、探活、恢复凭据落盘。

职责:

  • StartServe:选空闲端口、生成随机 secret、写任务物料、经 prochost 拉起 serve、 HTTP 探活等就绪、落 proc.json
  • Alive/Kill/LogTail:存活探测、回收、脱敏后的诊断尾部
  • ReadServeInfo:从 proc.json 重建 Proc,供 agentd 重启后 Resume

边界:

  • 不说 ACP、不解析事件:协议在 acp.go,语义在 adapter.go
  • 不做重试决策:探活失败只如实返回,重试与判死节奏归 adapter 的看门狗

为什么存活判据是「存活锁 + HTTP 端口探活」:锁证明 shim 还在,HTTP 证明 serve 本身还在应答。serve 崩了但 shim 尚未收尸的窗口由 HTTP 这条兜住;反过来锁没了 也就没有进程可探。grok serve 的根路径返回 404——能收到任何 HTTP 响应就说明 进程还在监听。

providercarry.go —— 从用户权威 grok 配置里搬运自定义 provider 定义。

职责:

  • 从 ~/.grok/config.toml 的文本里抽出 [model.*] 段与整个 [models] 段 (default 单独摘出,因为它另有优先级;其余键原样搬走)
  • 只做文本切段,不解析 TOML、不改写任何字段值

边界:

  • 不写文件:结果交 WriteTaskEnv 织进任务级 config.toml
  • 不判断字段含义、不识别密钥:段内字节原样搬运
  • 不搬 [ui] / [permission] / [cli]:那三段永远以 handoff 为准,搬过来 等于让用户的个人配置覆盖任务级权限隔离

为什么不引入 TOML 库:本仓零 TOML 依赖,且 WriteTaskEnv 本身就是手写字符串 拼接。「解析 + 再序列化」会重排键、丢掉用户注释,生成的 config 不再一眼可读; 原样搬字节连注释都保得住。代价是自己认段边界,已知边界见 extractProviderConfig。

日志纪律:只打段名与条数,任何情况下不打段内容、不打字段值——[model.*] 段里 有 api_key。与 authsync.go 文件头「不打 token 值」同源。

reap.go —— 无内存运行态时的确定性兜底回收。

职责:

  • Reap:按 proc.json 拿 prochost.Handle 并 Kill

边界:

  • 不碰任务状态(adapter 不写 store);回收不掉只返回错误,留不留事件是 manager 的事

resume.go —— agentd 重启后的执行恢复与 serve 存活看门狗。

职责:

  • Resume:读 serve.json → 端口探活 → 修复 auth 软链 → 重连 ACP → session/load → 重建事件循环
  • watchdog:周期探活,判死时以脱敏的 serve.log 尾部产出 failed result

边界:

  • 不判断「该不该恢复」的业务前提(如是否有未决权限工单)——那需要工单知识, 属 manager(见 manager.go 的 volatilePermitter)

看门狗节奏与 opencode 同规格:活跃期 200ms 高频,连续 fastProbes 次成功且无 新事件后降到 2s(任务挂夜里时省下每天数十万次探活),任一失败立即回高频; 连续 failThreshold 次失败才判死,吸收抖动不误杀。

spend.go —— grok 的**累计消耗**账目解析。

职责:把 session/prompt 响应的 result._meta 解析成一条 proto.SpendEntry。 边界:纯函数,不碰 runState、不发事件、不写日志——接线在 adapter.go。

与同目录 usage.go 的关系(**grok 是四家里最容易搞错的一家**):同一条 ACP 线 上有两套命名,且缓存的算法**相反**——

  • usage.go 取 _x.ai/session_notification 的 response_completed,字段是 snake_case(input_tokens / cache_read_input_tokens),**不含**缓存要相加, 解的是「当前 context 占用」;
  • 本文件取 session/prompt 响应的 _meta,字段是 camelCase (inputTokens / cachedReadTokens),**已含**缓存要相减, 解的是「整个回合消耗了多少」。

按字段名模糊匹配必错,grok 官方文档已确认这是它有意为之的两套投影。

symlinkcap.go —— grok 的符号链接能力探测。

职责:回答「本机现在能不能建符号链接」,供 agentd 决定是否注册 grok。

边界:

  • 只探测,不注册、不报错到调用方之外:结论由 agentd 呈现
  • 不缓存:agentd 启动时探一次即可,权限在运行中变化不是要覆盖的场景

为什么 grok 需要这个而别的执行器不需要:grok 给每个任务建一条指向用户 auth 文件的符号链接(taskenv.go 建、authsync.go 周期复位),而 Windows 上 建符号链接需要 SeCreateSymbolicLinkPrivilege(管理员)或开发者模式。

为什么不改成复制文件绕开:软链的意义是 auth 文件只有一份权威副本。改成 复制后,grok 在任务里刷新 token 写的是副本,用户那份与任务那份各自漂移, 且这种不一致是静默的——正是 B26 那一整类问题。宁可诚实拒绝,不静默降级。

taskenv.go —— grok 任务环境物料生成:任务级 GROK_HOME 与权限配置。

职责:

  • WriteTaskEnv:建 <taskDir>/grokhome 并写 config.toml(钉死 permission_mode 与第 0 层分级规则、注入任务级模型)
  • EnsureAuthLink:幂等地把 grokhome/auth.json 指向真实 ~/.grok/auth.json (serve 启动脚本与 secret 注入已随 tmux 拆除,改由 proc.go 的 Spec.Env 承担)

边界:

  • 不起进程、不连网络:进程在 proc.go,协议在 acp.go
  • 不读用户的真实 grok 配置(除 auth.json 软链外一律纯净)

为什么任务级 GROK_HOME 是必需而非可选:用户真实 ~/.grok/config.toml 常见 permission_mode = "always-approve",直接沿用等于所有工具调用自动放行、 permission 事件永不产生——审批门全废。任务级 home 把它钉死为 "default"。

为什么权限规则表比 opencode 短:grok 内建按 && / || / ; / 管道分段识别只读 命令并自动放行(ls/cat/git status/grep/rg 等),且 `ls && rm -rf /` 会拆开、 rm 段仍然拦。opencode 那张以 "*": "allow" 收尾的表是手工补的等价物,这里 只需补 ask 危险模式与 allow 编辑放行。

已知泄漏(关不掉):grok 无视 GROK_HOME,仍从真实 HOME 读 ~/.claude/settings*.json 与 ~/.claude/skills。缓解是 grok 的求值为 deny > ask > allow 跨源生效——本文件 写的 ask 压得过用户个人 allowlist 的 allow,第 0 层分级仍成立。

usage.go —— grok 的 token 用量与实际模型名解析。

职责:把两条 _x.ai/* 私有通知解析成 proto.Usage 与模型名/窗口。 边界:纯函数,不碰 runState、不发事件、不写日志——接线在 adapter.go。

为什么用量在私有通知上:grok 的 ACP 线把用量放在 _x.ai/session_notification 与 _x.ai/models/update 上,标准的 session/update 变体一个都不带计数。

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EnsureAuthLink(homeDir string) error

EnsureAuthLink 幂等地把 <homeDir>/auth.json 指向真实 ~/.grok/auth.json。

为什么必须可修复而非一次性建立:spike 实测任务级 home 的软链会在 token 刷新 前后消失,随后 session/new 直接返回 Authentication required。Start 与 Resume 都调本函数,成本为零。

为什么用软链而非拷贝:拷贝会让每个任务 home 各自持有凭据并独立刷新,而刷新 令牌轮换可能反噬用户本人的登录态——凭据只应有一个权威副本。

func SymlinkCapability added in v0.3.0

func SymlinkCapability(probeDir string) (supported bool, reason string)

SymlinkCapability 探测本机是否具备创建符号链接的能力。

参数:probeDir 为探测目录(用 DataDir,它一定存在且可写)

返回:

  • supported: 是否可用
  • reason: 不可用的原因,**含可行动的处置建议**;可用时为空串

注意:探测会在 probeDir 下建一个临时软链再删掉,正常路径不留任何残留。

func SyncAuthToAuthority

func SyncAuthToAuthority(homeDir string, log *slog.Logger) error

SyncAuthToAuthority 跑一轮凭据巡检:把任务 home 里 grok 自行刷新出的新凭据 收编进权威副本,并把任务侧恢复成软链。

参数:

  • homeDir: 任务级 GROK_HOME,即 <taskDir>/grokhome
  • log: 日志入口;nil 时退回 slog.Default()。调用方应传入已带 task 字段的 logger(本函数不认识 task id)

返回:**仅在「本轮确实该写回却写失败」时返回错误**。其余情况(无事可做、 任务侧损坏、权威侧缺失或损坏)都返回 nil——它们是可接受的稳态,不该让调用方 的看门狗把它当异常。

注意:

  • 绝大多数轮次在第一个 lstat 就返回,成本是一次系统调用,可以放心高频调用;
  • 「复位软链」与「收编」是两件独立的事:只要发现任务侧不是软链就该复位, 哪怕本轮没收编到任何东西(陈旧拷贝留着会让任务下次临期必死)。两处例外 写在下面的分支注释里。

func WriteTaskEnv

func WriteTaskEnv(taskDir, model string) (homeDir string, err error)

WriteTaskEnv 建任务级 GROK_HOME 并写入权限配置,返回该 home 目录路径。

参数:

  • taskDir: 任务工作目录(须已存在,由调用方保证)
  • model: 任务级模型;空则退回权威配置的 default,两者都空时不写 default (此时 [models] 段仍可能因搬来的辅助旋钮而存在)

返回:grokhome 目录路径;建目录或写文件失败时返回错误

注意:重复调用幂等覆盖,调用方可安全重试

Types

type ACPClient

type ACPClient struct {
	// contains filtered or unexported fields
}

ACPClient 是一条 ACP 连接。并发安全:nextID/pending 由 mu 保护; 写连接由 writeMu 串行化(websocket 不允许并发写)。

func DialACP

func DialACP(ctx context.Context, wsURL string, h ACPHandler, log *slog.Logger) (*ACPClient, error)

DialACP 连接 ACP 端点并启动读循环。

参数:

  • ctx: 仅控制握手阶段;连接生命周期延续到 Close
  • wsURL: 形如 ws://127.0.0.1:<port>/ws?server-key=<secret>
  • h: 回调面(不得为 nil)
  • log: 日志入口(nil 退回 slog.Default())

注意:wsURL 含 secret,**日志里绝不能打印它**(本函数只记录 host 与 path)。

func (*ACPClient) Call

func (c *ACPClient) Call(ctx context.Context, method string, params any) (json.RawMessage, error)

Call 发起请求并阻塞等待响应。

func (*ACPClient) CallAsync

func (c *ACPClient) CallAsync(method string, params any) (<-chan ACPResult, error)

CallAsync 发起请求并立即返回结果通道。

为什么需要它:session/prompt 要跑完一整个回合(可能几十分钟)才响应, Start 必须立即返回,不能阻塞在这上面。

func (*ACPClient) Close

func (c *ACPClient) Close() error

Close 关闭连接,所有挂起的请求以错误终结。

func (*ACPClient) Notify

func (c *ACPClient) Notify(method string, params any) error

Notify 发送通知(无需应答,用于 session/cancel)。

func (*ACPClient) Reply

func (c *ACPClient) Reply(reqID json.RawMessage, result any) error

Reply 应答对方请求(用于权限裁决回发)。reqID 必须是 OnPermission 收到的原值。

type ACPHandler

type ACPHandler interface {
	// OnNotify 收到对方通知(无 id 的消息)
	OnNotify(method string, params json.RawMessage)
	// OnAskQuestion 收到 _x.ai/ask_user_question(对方请求,**必须应答**)。
	// 不应答会让 session/prompt 永不返回、任务永久静止(spec §4.2.3 / §5.3(c) 实测)。
	OnAskQuestion(reqID json.RawMessage, params json.RawMessage)
	// OnPermission 收到 session/request_permission(对方请求,需应答)。
	// reqID 原样保存,裁决回来后经 Reply 回发。
	OnPermission(reqID json.RawMessage, params json.RawMessage)
	// OnClosed 连接终止(err 为终止原因,正常关闭时为 nil)
	OnClosed(err error)
}

ACPHandler 是 adapter 侧的回调面。实现方必须假定回调在读循环 goroutine 上 触发:**不得在回调里做阻塞操作**,否则会卡住整条连接的消息消费。

type ACPResult

type ACPResult struct {
	Result json.RawMessage
	Err    error
}

ACPResult 是一次异步调用的终局(二选一)。

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter 是 grok 的 executor.Adapter 实现。

并发安全:runs 表由 mu 保护;每个任务的运行态只被该任务自己的回调路径访问。

func New

func New(log *slog.Logger) *Adapter

New 创建 grok adapter。

参数:

  • log: 本模块日志入口(nil 时退回 slog.Default())

func (*Adapter) Events

func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent

Events 返回该任务的事件流通道(Start 后可用)。通道关闭表示执行终结。

func (*Adapter) PermissionsVolatile

func (a *Adapter) PermissionsVolatile() bool

PermissionsVolatile 表明本 adapter 的权限请求随连接消亡。

manager 据此在 agentd 重启后拒绝恢复「尚有未决权限工单」的任务——实测 session/load 只恢复会话历史,不恢复未决授权请求(见 spec §5.2)。

func (*Adapter) Probe

Probe 只读探测 grok serve 是否仍存活(manager 的 prober 可选接口)。

判据与 Resume 共用同一份 Proc.Alive:存活锁被持有且端口收到任何 HTTP 响应 (含 404)。

返回:

  • err != nil:探不出结论(proc.json 缺失/损坏),调用方按 unknown 处理

func (*Adapter) ProcHandle

func (a *Adapter) ProcHandle(taskID, taskDir string) (prochost.Handle, error)

ProcHandle 交出该任务的进程句柄(来自任务目录的 proc.json)。

参数:

  • taskID: 任务 ID,仅用于日志定位
  • taskDir: 任务目录(凭据所在)

返回:

  • 进程句柄;proc.json 不存在或不可解析时返回错误

注意:本方法**只读**,不探活、不发信号——存活判定与回收分别是 prochost.Alive 与 prochost.Sweep 的职责。agentd 以可选接口消费它 (不实现该方法的 adapter 一律按「无凭据」降级,与 reaper/prober 同款路数)。

func (*Adapter) Reap

func (a *Adapter) Reap(taskID, taskDir string) error

Reap 在没有内存运行态时按 proc.json 兜底回收 executor 侧资源。

回收顺序:读 proc.json 拿 Handle → prochost.Kill(内部先试锁,锁空闲直接成功)。

为什么不再有「确定性命名兜底」:旧实现在 proc.json 缺失时退到 tmux 会话名 handoff-<id8>,因为会话名可由 taskID 推导。锁+pid 无法从 taskID 推导, proc.json 缺失就是真的无据可查——如实报错交协调者,不猜。

返回:Handle 对应的进程本就不在时返回 nil——目标是「确保它没了」, 不是「确保我杀了它」。

func (*Adapter) RespondPermission

func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision, _ string) (err error)

RespondPermission 应答 grok 的权限请求。

参数:

  • taskID: 目标任务
  • permID: 权限请求 id(即 ACP 的 toolCallId,裸值不带命名空间前缀)
  • decision: "once"(批准本次)或 "reject"(拒绝)
  • reason: ACP 的 outcome 只有 optionId,带不了消息,本 adapter 忽略(spec §2.5)

返回:

  • 任务不在运行中、或挂起表查不到该 permID 时,包装 executor.ErrTaskNotRunning ——两者都意味着「executor 侧那次请求已经不在了」,调用方据此转失败交协调者, 而不是当作可重试的瞬时错误

func (*Adapter) Resume

func (a *Adapter) Resume(req executor.ResumeReq) (out executor.ResumeOutcome, err error)

Resume 尝试恢复一个 agentd 重启前已在执行的任务。

参数:

  • req: 恢复请求(TaskDir 是 serve.json 与 grokhome 所在,即 DataDir/tasks/<id>; RepoPath 是 ACP session/load 的 cwd;SessionID 是落库的会话 id)

返回:

  • Alive=true:serve 存活、ACP 已重连、会话已载入、事件流已重建; Mode 说明走到的级别(reattach 热重连 / cold 冷恢复 / fresh 新会话)
  • Alive=false:serve 已不在且不允许冷恢复、或凭据缺失,调用方据此转 failed 交协调者

func (*Adapter) Send

func (a *Adapter) Send(ctx context.Context, taskID, text string) error

Send 回答提问 / 回发修改指令,对同一会话续接执行。text 原样透传不加工。

func (*Adapter) Start

func (a *Adapter) Start(ctx context.Context, req executor.StartReq) (err error)

Start 异步启动执行并立即返回。

步骤:物料与 serve(StartServe)→ ACP 连接 → initialize → session/new → 不等待地发 session/prompt → emit progress{SessionID}「会话就绪」。

注意:session/prompt 要跑完一整个回合才响应,因此用 CallAsync 并由独立 goroutine 等待其终局作回合边界。

func (*Adapter) Stop

func (a *Adapter) Stop(taskID string) error

Stop 终止执行并回收资源:置 stopping → 关 ACP → kill 执行者进程组 → 关事件通道。

type Proc

type Proc struct {
	Handle  prochost.Handle `json:"handle"`
	TaskDir string          `json:"task_dir"` // 任务目录
	Port    int             `json:"port"`
	Secret  string          `json:"secret"`
}

Proc 是一个 grok serve 实例的句柄与恢复凭据。

注意:Secret 字段是明文 secret,序列化后的 proc.json 必须 0600, 且任何日志/错误文本输出前都要经 LogTail 之类的脱敏路径。

func ReadServeInfo

func ReadServeInfo(taskDir string) (*Proc, error)

ReadServeInfo 从任务目录读回 Proc,供 agentd 重启后 Resume。

func StartServe

func StartServe(ctx context.Context, repoPath, taskID, markRoot, taskDir, model string, env []string, log *slog.Logger) (*Proc, error)

StartServe 经 prochost 拉起一个任务专属的 grok serve 并等其就绪。

参数:

  • ctx: 控制启动阶段的超时/取消
  • repoPath: 任务工作目录(serve 的 cwd)
  • taskID: 任务 ID(日志与 proc.json 定位)
  • taskDir: 任务物料目录
  • model: 任务级模型(空=用 grok 默认)
  • env: 注入到 serve 进程的环境变量(形如 KEY=VALUE,已由 manager 解析展开); 命中 protectedEnvKeys 的条目会被 handoff 自身注入覆盖并打 WARN

返回:就绪的 Proc;任一步失败返回错误(错误携带脱敏后的 serve.log 尾部)

func (*Proc) Alive

func (p *Proc) Alive() bool

Alive 检查 grok serve 是否仍然存活:存活锁被持有 且 HTTP 端口有应答。

为什么第一条是锁而不是端口:锁是本地文件操作、微秒级,端口探测要走网络栈 且失败时要等超时。锁判死就没必要再探端口了。 (旧实现第一条是 tmux has-session,而会话里第二窗口的 tail -f 会一直活着、 serve 早死了会话依然存在——那个假存活来源已随 tmux 一起消失。)

func (*Proc) Kill

func (p *Proc) Kill() error

Kill 终止 grok serve 及其后代(按进程组),幂等。

func (*Proc) LogTail

func (p *Proc) LogTail() string

LogTail 返回脱敏后的 serve.log 尾部,供启动超时与死亡诊断。

为什么必须脱敏:这段尾部会进 Result.FailReason(落事件库)与 agentd.log, 而它的内容完全由 grok 决定——实测启动横幅就会原样打印 Secret 一行。

func (*Proc) WSURL

func (p *Proc) WSURL() string

WSURL 返回 ACP 的 WebSocket 端点。

注意:返回值含 secret,**绝不可整体写进日志**。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL