dsh-grok-provider 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.en.md +200 -0
- package/README.md +200 -0
- package/SECURITY.md +35 -0
- package/dist/client/client.js +215 -0
- package/dist/host/index.mjs +147 -0
- package/dist/internal/account-dashboard.mjs +67 -0
- package/dist/internal/auth-controller.mjs +184 -0
- package/dist/internal/auth-registry.mjs +64 -0
- package/dist/internal/auth-rpc.mjs +118 -0
- package/dist/internal/billing-summary.mjs +105 -0
- package/dist/internal/credential-source.mjs +168 -0
- package/dist/internal/grok-adapter.mjs +160 -0
- package/dist/internal/grok-command-handler.mjs +62 -0
- package/dist/internal/grok-command.mjs +17 -0
- package/dist/internal/grok-transport.mjs +279 -0
- package/dist/internal/model-catalog.mjs +142 -0
- package/dist/internal/official-auth-driver.mjs +40 -0
- package/dist/internal/official-cli-auth.mjs +231 -0
- package/dist/internal/official-cli-verifier.mjs +62 -0
- package/dist/internal/official-credential-loader.mjs +64 -0
- package/dist/internal/provider-runtime.mjs +43 -0
- package/dist/internal/responses-codec.mjs +357 -0
- package/dist/internal/responses-request.mjs +246 -0
- package/dist/internal/responses-sse.mjs +123 -0
- package/docs/01-product-requirements.md +128 -0
- package/docs/02-architecture-options.md +180 -0
- package/docs/03-security-threat-model.md +243 -0
- package/docs/04-harness-contract.md +325 -0
- package/docs/05-test-plan.md +224 -0
- package/docs/06-release-plan.md +182 -0
- package/docs/07-decision-gate.md +76 -0
- package/docs/08-upstream-cli-1.0.5-evidence.md +98 -0
- package/docs/09-implementation-status.md +37 -0
- package/docs/README.md +60 -0
- package/docs/adr/0001-auth-and-transport-route.md +86 -0
- package/docs/adr/0002-v0.1-scope.md +46 -0
- package/docs/adr/0003-dual-authentication.md +77 -0
- package/docs/adr/0004-dynamic-model-catalog.md +34 -0
- package/docs/adr/0005-official-cli-only-authentication.md +36 -0
- package/docs/adr/0006-account-dashboard.md +84 -0
- package/grok-provider.patch.yml +3 -0
- package/package.json +92 -0
- package/types/index.d.ts +9 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
2
|
+
const MAX_TEXT_LENGTH = 8 * 1024 * 1024
|
|
3
|
+
const MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
|
4
|
+
const MAX_MESSAGES = 10_000
|
|
5
|
+
const MAX_TOOLS = 128
|
|
6
|
+
|
|
7
|
+
export class UnsupportedResponsesRequestError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("The Harness request contains content or options unsupported by Grok Responses")
|
|
10
|
+
this.name = "UnsupportedResponsesRequestError"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function encodeResponsesRequest(options) {
|
|
15
|
+
if (
|
|
16
|
+
!isPlainObject(options) ||
|
|
17
|
+
options.provider !== "grok" ||
|
|
18
|
+
!isId(options.model) ||
|
|
19
|
+
!Array.isArray(options.messages) ||
|
|
20
|
+
options.messages.length > MAX_MESSAGES
|
|
21
|
+
) {
|
|
22
|
+
throw new UnsupportedResponsesRequestError()
|
|
23
|
+
}
|
|
24
|
+
if (options.stop !== undefined && (!Array.isArray(options.stop) || options.stop.length > 0)) {
|
|
25
|
+
throw new UnsupportedResponsesRequestError()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const request = {
|
|
29
|
+
model: options.model,
|
|
30
|
+
input: encodeMessages(options.messages, options.model),
|
|
31
|
+
...(options.system === undefined ? {} : { instructions: parseText(options.system) }),
|
|
32
|
+
...(options.tools === undefined ? {} : { tools: encodeTools(options.tools) }),
|
|
33
|
+
...(options.reasoningEffort === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { reasoning: { effort: parseId(options.reasoningEffort) } }),
|
|
36
|
+
...(options.temperature === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { temperature: parseTemperature(options.temperature) }),
|
|
39
|
+
...(options.maxTokens === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { max_output_tokens: parseMaxTokens(options.maxTokens) }),
|
|
42
|
+
include: ["reasoning.encrypted_content"],
|
|
43
|
+
stream: true,
|
|
44
|
+
store: false,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let serialized
|
|
48
|
+
try {
|
|
49
|
+
serialized = JSON.stringify(request)
|
|
50
|
+
} catch {
|
|
51
|
+
throw new UnsupportedResponsesRequestError()
|
|
52
|
+
}
|
|
53
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_REQUEST_BYTES) {
|
|
54
|
+
throw new UnsupportedResponsesRequestError()
|
|
55
|
+
}
|
|
56
|
+
return request
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function encodeMessages(messages, targetModel) {
|
|
60
|
+
const input = []
|
|
61
|
+
for (const message of messages) {
|
|
62
|
+
if (!isPlainObject(message) || !Array.isArray(message.content)) fail()
|
|
63
|
+
if (message.source?.kind === "tool") {
|
|
64
|
+
encodeToolResultMessage(message, input)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (message.role !== "user" && message.role !== "assistant" && message.role !== "system") fail()
|
|
68
|
+
encodeOrdinaryMessage(message, input, targetModel)
|
|
69
|
+
}
|
|
70
|
+
return input
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function encodeOrdinaryMessage(message, input, targetModel) {
|
|
74
|
+
const replayBlocks = readReplayBlocks(message, targetModel)
|
|
75
|
+
let text = ""
|
|
76
|
+
const flushText = () => {
|
|
77
|
+
if (text.length === 0) return
|
|
78
|
+
input.push({ role: message.role, content: text })
|
|
79
|
+
text = ""
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [position, block] of message.content.entries()) {
|
|
83
|
+
if (!isPlainObject(block) || typeof block.type !== "string") fail()
|
|
84
|
+
if (block.type === "text") {
|
|
85
|
+
text += parseText(block.text)
|
|
86
|
+
if (text.length > MAX_TEXT_LENGTH) fail()
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (block.type === "reasoning" && message.role === "assistant") {
|
|
90
|
+
const replay = replayBlocks?.[position]
|
|
91
|
+
if (isReasoningReplay(replay)) {
|
|
92
|
+
flushText()
|
|
93
|
+
input.push({
|
|
94
|
+
type: "reasoning",
|
|
95
|
+
id: replay.id,
|
|
96
|
+
encrypted_content: replay.encryptedContent,
|
|
97
|
+
summary: block.text.length === 0
|
|
98
|
+
? []
|
|
99
|
+
: [{ type: "summary_text", text: parseText(block.text) }],
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (block.type === "tool-call" && message.role === "assistant") {
|
|
105
|
+
flushText()
|
|
106
|
+
if (!isId(block.id) || !isId(block.name) || !isJsonObject(block.arguments)) fail()
|
|
107
|
+
input.push({
|
|
108
|
+
type: "function_call",
|
|
109
|
+
call_id: block.id,
|
|
110
|
+
name: block.name,
|
|
111
|
+
arguments: block.arguments,
|
|
112
|
+
})
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
fail()
|
|
116
|
+
}
|
|
117
|
+
flushText()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readReplayBlocks(message, targetModel) {
|
|
121
|
+
const source = message.source
|
|
122
|
+
if (
|
|
123
|
+
!isPlainObject(source) ||
|
|
124
|
+
source.kind !== "model" ||
|
|
125
|
+
source.provider !== "grok" ||
|
|
126
|
+
source.model !== targetModel ||
|
|
127
|
+
!isPlainObject(source.replayState) ||
|
|
128
|
+
!isPlainObject(source.replayState.response) ||
|
|
129
|
+
source.replayState.response.version !== 1 ||
|
|
130
|
+
!Array.isArray(source.replayState.blocks) ||
|
|
131
|
+
source.replayState.blocks.length !== message.content.length
|
|
132
|
+
) return undefined
|
|
133
|
+
return source.replayState.blocks
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isReasoningReplay(value) {
|
|
137
|
+
return isPlainObject(value) &&
|
|
138
|
+
value.type === "reasoning" &&
|
|
139
|
+
isId(value.id) &&
|
|
140
|
+
typeof value.encryptedContent === "string" &&
|
|
141
|
+
value.encryptedContent.length > 0 &&
|
|
142
|
+
Buffer.byteLength(value.encryptedContent, "utf8") <= MAX_TEXT_LENGTH
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function encodeToolResultMessage(message, input) {
|
|
146
|
+
if (
|
|
147
|
+
message.role !== "user" ||
|
|
148
|
+
message.content.length !== 1 ||
|
|
149
|
+
!isPlainObject(message.content[0]) ||
|
|
150
|
+
message.content[0].type !== "tool-result"
|
|
151
|
+
) fail()
|
|
152
|
+
const result = message.content[0]
|
|
153
|
+
if (
|
|
154
|
+
!isId(message.source.callId) ||
|
|
155
|
+
result.toolCallId !== message.source.callId ||
|
|
156
|
+
!Array.isArray(result.content)
|
|
157
|
+
) fail()
|
|
158
|
+
|
|
159
|
+
let output = ""
|
|
160
|
+
for (const block of result.content) {
|
|
161
|
+
if (!isPlainObject(block) || block.type !== "text") fail()
|
|
162
|
+
output += parseText(block.text)
|
|
163
|
+
if (output.length > MAX_TEXT_LENGTH) fail()
|
|
164
|
+
}
|
|
165
|
+
input.push({ type: "function_call_output", call_id: result.toolCallId, output })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function encodeTools(tools) {
|
|
169
|
+
if (!Array.isArray(tools) || tools.length > MAX_TOOLS) fail()
|
|
170
|
+
const names = new Set()
|
|
171
|
+
return tools.map((tool) => {
|
|
172
|
+
if (
|
|
173
|
+
!isPlainObject(tool) ||
|
|
174
|
+
!isId(tool.name) ||
|
|
175
|
+
names.has(tool.name) ||
|
|
176
|
+
typeof tool.description !== "string" ||
|
|
177
|
+
tool.description.length > 4096 ||
|
|
178
|
+
!isPlainObject(tool.parameters)
|
|
179
|
+
) fail()
|
|
180
|
+
names.add(tool.name)
|
|
181
|
+
return {
|
|
182
|
+
type: "function",
|
|
183
|
+
name: tool.name,
|
|
184
|
+
description: tool.description,
|
|
185
|
+
parameters: cloneJsonObject(tool.parameters),
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cloneJsonObject(value) {
|
|
191
|
+
let serialized
|
|
192
|
+
let cloned
|
|
193
|
+
try {
|
|
194
|
+
serialized = JSON.stringify(value)
|
|
195
|
+
if (serialized === undefined || Buffer.byteLength(serialized, "utf8") > 1024 * 1024) fail()
|
|
196
|
+
cloned = JSON.parse(serialized)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof UnsupportedResponsesRequestError) throw error
|
|
199
|
+
fail()
|
|
200
|
+
}
|
|
201
|
+
if (!isPlainObject(cloned)) fail()
|
|
202
|
+
return cloned
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseText(value) {
|
|
206
|
+
if (typeof value !== "string" || value.length > MAX_TEXT_LENGTH) fail()
|
|
207
|
+
return value
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseId(value) {
|
|
211
|
+
if (!isId(value)) fail()
|
|
212
|
+
return value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseTemperature(value) {
|
|
216
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 2) fail()
|
|
217
|
+
return value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseMaxTokens(value) {
|
|
221
|
+
if (!Number.isSafeInteger(value) || value <= 0) fail()
|
|
222
|
+
return value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isJsonObject(value) {
|
|
226
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 2 * 1024 * 1024) return false
|
|
227
|
+
try {
|
|
228
|
+
return isPlainObject(JSON.parse(value))
|
|
229
|
+
} catch {
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isId(value) {
|
|
235
|
+
return typeof value === "string" && ID_PATTERN.test(value)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function fail() {
|
|
239
|
+
throw new UnsupportedResponsesRequestError()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isPlainObject(value) {
|
|
243
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
244
|
+
const prototype = Object.getPrototypeOf(value)
|
|
245
|
+
return prototype === Object.prototype || prototype === null
|
|
246
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const MAX_EVENT_BYTES = 2 * 1024 * 1024
|
|
2
|
+
const MAX_STREAM_BYTES = 128 * 1024 * 1024
|
|
3
|
+
const MAX_EVENTS = 100_000
|
|
4
|
+
|
|
5
|
+
export class InvalidResponsesSseError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("The Grok Responses event stream is invalid")
|
|
8
|
+
this.name = "InvalidResponsesSseError"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function* parseResponsesSse(source) {
|
|
13
|
+
if (source === null || source === undefined || typeof source[Symbol.asyncIterator] !== "function") {
|
|
14
|
+
throw new TypeError("Responses SSE source must be an async iterable")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const decoder = new TextDecoder("utf-8", { fatal: true })
|
|
18
|
+
let buffer = ""
|
|
19
|
+
let totalBytes = 0
|
|
20
|
+
let eventBytes = 0
|
|
21
|
+
let eventName
|
|
22
|
+
let dataLines = []
|
|
23
|
+
let eventCount = 0
|
|
24
|
+
let done = false
|
|
25
|
+
|
|
26
|
+
const dispatch = () => {
|
|
27
|
+
if (dataLines.length === 0) {
|
|
28
|
+
eventName = undefined
|
|
29
|
+
eventBytes = 0
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
const data = dataLines.join("\n")
|
|
33
|
+
dataLines = []
|
|
34
|
+
eventBytes = 0
|
|
35
|
+
if (data === "[DONE]") {
|
|
36
|
+
if (eventName !== undefined) fail()
|
|
37
|
+
done = true
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
if (done) fail()
|
|
41
|
+
|
|
42
|
+
let value
|
|
43
|
+
try {
|
|
44
|
+
value = JSON.parse(data)
|
|
45
|
+
} catch {
|
|
46
|
+
fail()
|
|
47
|
+
}
|
|
48
|
+
if (!isPlainObject(value) || typeof value.type !== "string") fail()
|
|
49
|
+
if (eventName !== undefined && eventName !== value.type) fail()
|
|
50
|
+
eventName = undefined
|
|
51
|
+
eventCount += 1
|
|
52
|
+
if (eventCount > MAX_EVENTS) fail()
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const consumeLine = (rawLine) => {
|
|
57
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine
|
|
58
|
+
if (line === "") return dispatch()
|
|
59
|
+
if (line.startsWith(":")) return undefined
|
|
60
|
+
if (done) fail()
|
|
61
|
+
|
|
62
|
+
const colon = line.indexOf(":")
|
|
63
|
+
const field = colon === -1 ? line : line.slice(0, colon)
|
|
64
|
+
let value = colon === -1 ? "" : line.slice(colon + 1)
|
|
65
|
+
if (value.startsWith(" ")) value = value.slice(1)
|
|
66
|
+
eventBytes += Buffer.byteLength(line, "utf8") + 1
|
|
67
|
+
if (eventBytes > MAX_EVENT_BYTES) fail()
|
|
68
|
+
|
|
69
|
+
if (field === "event") {
|
|
70
|
+
if (eventName !== undefined || value.length === 0 || value.length > 256) fail()
|
|
71
|
+
eventName = value
|
|
72
|
+
} else if (field === "data") {
|
|
73
|
+
dataLines.push(value)
|
|
74
|
+
} else if (field !== "id") {
|
|
75
|
+
fail()
|
|
76
|
+
}
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
for await (const chunk of source) {
|
|
82
|
+
if (!(chunk instanceof Uint8Array)) fail()
|
|
83
|
+
totalBytes += chunk.byteLength
|
|
84
|
+
if (totalBytes > MAX_STREAM_BYTES) fail()
|
|
85
|
+
buffer += decoder.decode(chunk, { stream: true })
|
|
86
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_EVENT_BYTES) fail()
|
|
87
|
+
|
|
88
|
+
let newline
|
|
89
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
90
|
+
const value = consumeLine(buffer.slice(0, newline))
|
|
91
|
+
buffer = buffer.slice(newline + 1)
|
|
92
|
+
if (value !== undefined) yield value
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
buffer += decoder.decode()
|
|
96
|
+
if (buffer.length > 0) {
|
|
97
|
+
const value = consumeLine(buffer)
|
|
98
|
+
if (value !== undefined) yield value
|
|
99
|
+
}
|
|
100
|
+
if (dataLines.length > 0 || eventName !== undefined) {
|
|
101
|
+
const value = dispatch()
|
|
102
|
+
if (value !== undefined) yield value
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof InvalidResponsesSseError || error instanceof TypeError) throw error
|
|
106
|
+
if (error?.name === "AbortError") throw error
|
|
107
|
+
throw new InvalidResponsesSseError()
|
|
108
|
+
} finally {
|
|
109
|
+
buffer = ""
|
|
110
|
+
dataLines = []
|
|
111
|
+
eventName = undefined
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fail() {
|
|
116
|
+
throw new InvalidResponsesSseError()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isPlainObject(value) {
|
|
120
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
121
|
+
const prototype = Object.getPrototypeOf(value)
|
|
122
|
+
return prototype === Object.prototype || prototype === null
|
|
123
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# 产品需求
|
|
2
|
+
|
|
3
|
+
## 1. 产品定义
|
|
4
|
+
|
|
5
|
+
提供一个原创的 DeepSeek Harness LLM Provider,使 Harness 能把 Grok Build 当作模型后端使用,同时保留 Harness 自己的会话、权限、工具和附件边界。
|
|
6
|
+
|
|
7
|
+
冻结的 npm 身份:
|
|
8
|
+
|
|
9
|
+
- 包名:`dsh-grok-provider`
|
|
10
|
+
- 首个精确版本:`0.1.0`
|
|
11
|
+
- Provider ID:`grok`
|
|
12
|
+
- 当前真机模型快照:`grok-4.6`、`grok-4.5`;生产目录动态发现账号可用的全部模型
|
|
13
|
+
|
|
14
|
+
项目选择无需 scope 所有权的唯一名称,避免把脚手架和 credential owner 绑定到尚未确认的 `@yukiryou` scope。该名称在 2026-08-25 查询时未公开发布;发布前仍必须重新检查占用状态。
|
|
15
|
+
|
|
16
|
+
## 2. P0 用户目标
|
|
17
|
+
|
|
18
|
+
- 在 Harness `0.1.1-rc.2` 中安装精确 npm 版本后出现 Grok Provider,并动态列出当前账号通过 Grok Build 可用的全部模型。
|
|
19
|
+
- macOS 和 Windows 用户使用相同的插件包,不需要平台专属脚本。
|
|
20
|
+
- 用户可从 Harness 发起官方 CLI 浏览器登录;插件能识别登录中、成功、取消、失败、凭据过期和未登录状态。
|
|
21
|
+
- 首版只接受与发布绑定 CLI 版本的 xAI 第一方 OIDC schema 相符的候选。CLI 有效配置若选择外部 auth provider、企业 OIDC、API key 或无法判定的凭据结构,插件必须显示“不受支持的认证配置”并拒绝由本插件把该 token 发给 xAI CLI Chat Proxy;这不把未签名 metadata 宣称成来源证明。
|
|
22
|
+
- 支持多轮文本对话、reasoning 增量、流式文本、工具调用、usage 和明确 finish 原因。
|
|
23
|
+
- Harness 中止请求时,网络流和解析器都能及时终止。
|
|
24
|
+
- 热更新设置或重新认证不会让同一调用混用两组路由或凭据。
|
|
25
|
+
- 凭据不进入 renderer、RPC、settings、workspace、本插件日志、错误详情、诊断包或 npm tarball;官方 CLI 自身的日志、网络与遥测属于独立 vendor boundary。
|
|
26
|
+
- 本插件拥有并注入 Bearer 的推理请求只到固定 xAI origin,任何 3xx 都失败;官方 CLI 登录网络是独立信任边界。
|
|
27
|
+
- 发布物在 macOS arm64 的受管安装、重启和真实聊天 smoke test 中通过;Windows x64 首次真机验证在 `0.1.0` 发布后执行,验证前必须明确标注为“代码支持、真机未验证”。
|
|
28
|
+
|
|
29
|
+
## 3. P0 安全负需求
|
|
30
|
+
|
|
31
|
+
首版明确禁止:
|
|
32
|
+
|
|
33
|
+
- 复制、提取或反编译第三方/xAI 官方 CLI 的 OAuth Client ID,或接受用户提供任意 client ID。
|
|
34
|
+
- 保存 client secret,或把官方 access/refresh token 复制到 Harness credentials、settings、环境、日志、workspace 或 renderer。
|
|
35
|
+
- 由插件直接启动 shell、PowerShell、`cmd /c start`、`open`、`xdg-open` 或任意 URL opener;本约束不伪装成对官方 CLI 内部行为的保证。
|
|
36
|
+
- 通过 renderer/RPC 接收任意可执行路径、命令、参数、环境变量或 cwd;唯一允许的进程入口是 Host 内部验证后的官方 `grok`,参数只能来自闭合命令表。
|
|
37
|
+
- 接受用户、模型、远端响应或 marketplace 配置提供的 `baseURL`。
|
|
38
|
+
- 跟随重定向发送 Authorization。
|
|
39
|
+
- 把 Authorization、Cookie 或 Referer 带到图片、附件或远端返回的 URL。
|
|
40
|
+
- 将远端错误 body、请求 headers、完整 SSE 事件或凭据文件内容原样返回 UI。
|
|
41
|
+
- 在安装期间运行 `preinstall`、`install` 或 `postinstall`。
|
|
42
|
+
|
|
43
|
+
## 4. `0.1.0` 范围
|
|
44
|
+
|
|
45
|
+
### 账户与模型概览
|
|
46
|
+
|
|
47
|
+
- Web 设置页以账户状态卡、额度卡、模型能力卡的层级展示信息,并适配窄屏单列布局。
|
|
48
|
+
- 登录后展示官方 billing 返回的真实使用百分比和额度周期结束时间;数据缺失时明确显示不可用,不猜测。
|
|
49
|
+
- 展示账号动态可见的全部模型,以及上下文窗口、推理档位、文本/流式/tool capability。
|
|
50
|
+
- 首版不提供模型隐藏开关;Harness 模型选择器继续显示账号可见的全部模型。
|
|
51
|
+
- 页面支持手动刷新;额度与模型不写入插件配置或 workspace。
|
|
52
|
+
|
|
53
|
+
包含:
|
|
54
|
+
|
|
55
|
+
- 通过固定 `/v1/models` 动态发现的全部账号可用 Grok Build 模型;当前真实快照为 `grok-4.6` 与 `grok-4.5`。
|
|
56
|
+
- 文本输入与输出。
|
|
57
|
+
- reasoning 增量(远端协议实际支持时)。
|
|
58
|
+
- Harness 定义的工具调用增量。
|
|
59
|
+
- 流式 usage、finish、超时、取消与稳定错误码。
|
|
60
|
+
- 只读官方 Grok CLI 会话凭据。
|
|
61
|
+
- Host 侧官方 CLI 登录桥:`login`、`cancel`、`status`、`logout`。
|
|
62
|
+
- Web 设置页:认证状态、“使用 Grok 登录”、取消、退出、安装说明和隐私说明。
|
|
63
|
+
- TUI 闭合命令:`/grok status`、`login`、`cancel`、`logout`;Web 与 TUI 共用同一认证协调器。
|
|
64
|
+
|
|
65
|
+
不包含:
|
|
66
|
+
|
|
67
|
+
- 插件自管 OAuth、device flow、authorization-code callback、任意 URL opener、client ID 或 client secret。
|
|
68
|
+
- xAI API Key 模式。
|
|
69
|
+
- Grok ACP 或 `grok -p` headless 代理。
|
|
70
|
+
- 厂商侧 Web Search、X Search、远程抓取。
|
|
71
|
+
- 图片生成、图片 URL 下载或文件落盘。
|
|
72
|
+
- 图片输入;后续版本只有在 Harness attachment 限额与协议兼容测试完成后再考虑。
|
|
73
|
+
- 自定义 endpoint、企业 OIDC、自定义代理或多账号。
|
|
74
|
+
- 自动安装或更新 Grok CLI。
|
|
75
|
+
- 在远程 Web/headless 主机自动打开浏览器或无人值守登录的承诺。
|
|
76
|
+
- Linux 的发布承诺;实现应避免无谓的平台绑定,但首版只验收 macOS 与 Windows。
|
|
77
|
+
|
|
78
|
+
## 5. 用户流程
|
|
79
|
+
|
|
80
|
+
### 首次使用
|
|
81
|
+
|
|
82
|
+
1. 用户从 xAI 官方渠道安装与本机架构匹配的 Grok Build CLI。
|
|
83
|
+
2. 用户在 Web 设置页点击“使用 Grok 登录”,或在 TUI 输入 `/grok login`。
|
|
84
|
+
3. Host 从官方默认目录解析并对 `grok`/`grok.exe` 做路径、owner 和版本约束,通过 Harness `ctx.subprocess` 用固定 argv `[constrainedExecutable, "login", "--oauth"]` 启动它;该 seam 不做 shell 解释。
|
|
85
|
+
4. 在受支持的标准配置下,官方 CLI 打开系统浏览器、处理 OAuth/loopback callback,并管理自己的共享凭据;它可能先清除旧会话并在成功后同步 managed config。
|
|
86
|
+
5. 插件只向 UI 返回 `starting`、`running`、`succeeded`、`cancelled`、`failed` 等可观察闭合状态;不猜测“浏览器已打开”,也不回传原始 stdout/stderr、授权 URL 或 token。
|
|
87
|
+
6. 登录进程以成功、失败、取消或超时结算后,插件都先等待受管树、失效缓存并重新检查凭据;只有唯一、非歧义且符合绑定 CLI 版本第一方 OIDC schema 的记录才能把当前 credential 状态标成 `valid`,其他模式失败关闭。登录尝试 outcome 与当前 credential 状态分开显示。
|
|
88
|
+
7. 插件刷新固定 `/v1/models` 目录,用户选择当前账号可用模型并开始对话。
|
|
89
|
+
|
|
90
|
+
### 凭据过期
|
|
91
|
+
|
|
92
|
+
1. 官方 CLI 凭据过期或进入固定 skew 时,插件只对完全匹配的官方 OIDC record 启动一次 single-flight、30 秒有界的 `grok models`,由 CLI 自行刷新其凭据;随后重新读取并校验。刷新失败、CLI capability 缺失或 record 仍无效时以 LLM `AUTH` 失败,不循环、不降级。
|
|
93
|
+
2. 首个 401 使内存中的 lease 失效;已经发送的 POST 不自动重放。
|
|
94
|
+
3. 设置页显示“重新登录”;下一次明确用户动作才启动官方 CLI 浏览器登录。
|
|
95
|
+
4. 插件不删除、不修改官方 `auth.json`。
|
|
96
|
+
|
|
97
|
+
### 退出
|
|
98
|
+
|
|
99
|
+
Web 的“退出”或 TUI `/grok logout` 先中止本插件所有在途 Grok 请求并推进认证 generation,再由 Host 以同样的受限方式执行官方 `grok logout`,最后清除插件内存缓存。较早请求不得在退出后重新填充旧 token。插件不直接删除或修改其他应用的凭据文件。
|
|
100
|
+
|
|
101
|
+
## 6. 隐私与可观察性
|
|
102
|
+
|
|
103
|
+
- 提示词、工具参数、附件、搜索词默认不写日志。
|
|
104
|
+
- 普通日志只允许 endpoint ID、状态码、耗时、字节计数、插件错误码和随机 diagnostic ID;Harness RPC correlation 由 carrier 内部所有。
|
|
105
|
+
- 设置页明确说明提示词与工具结果会发送给 xAI Grok Build 服务。
|
|
106
|
+
- 插件只能保证自己不声明厂商侧搜索工具,不能保证服务商内部永不检索;此残余行为需依据 xAI 当时文档披露。
|
|
107
|
+
|
|
108
|
+
## 7. 成功指标
|
|
109
|
+
|
|
110
|
+
- 受管市场安装结果为 `artifact-verified`。
|
|
111
|
+
- macOS arm64 和 Windows x64 的自动测试通过。macOS x64 不在当前官方 CLI 支持矩阵,也不属于 `0.1.0` 承诺。
|
|
112
|
+
- `0.1.0` 发布前至少一台真实 macOS arm64 设备完成安装、登录、流式对话、工具调用、中止、重启与重新认证 smoke。
|
|
113
|
+
- `0.1.0` 发布后在一台真实 Windows x64 设备对 Registry 中的精确 `0.1.0` 执行首次安装、登录、流式对话、工具调用、中止、重启与重新认证 smoke;该项是发布后跟进,不回溯阻断已经完成的首次发布。
|
|
114
|
+
- `0.1.1` 及后续版本不把重复真机 smoke 设为常规发版门禁;由 macOS/Windows CI、契约测试、干净安装和精确 tarball 校验承接。认证、官方 CLI、Harness subprocess 或平台安全边界变化时安排定向真机复核,但除非当次发布另行声明,不作为强制门禁。
|
|
115
|
+
- canary secret 扫描确认日志、RPC、错误、临时文件和打包产物无泄漏。
|
|
116
|
+
- 协议测试确认第二个测试 origin 永远收不到 Authorization。
|
|
117
|
+
|
|
118
|
+
## 8. 发布阻断项
|
|
119
|
+
|
|
120
|
+
任一条件不满足都不得发布:
|
|
121
|
+
|
|
122
|
+
- xAI 官方文档或官方答复不允许第三方本地 adapter 使用官方会话凭据调用 CLI Chat Proxy。
|
|
123
|
+
- 服务条款或官方答复不允许独立插件使用该路径。
|
|
124
|
+
- 任一真实发现模型的基础流无法映射,或声明支持的工具调用无法无损映射到 Harness。
|
|
125
|
+
- 凭据只能通过不安全的 renderer、RPC 或明文复制方式获得。
|
|
126
|
+
- 官方 CLI 凭据筛选无法阻止 schema 不符 token 进入固定 Proxy。
|
|
127
|
+
- macOS arm64 发布前验收失败;或自动化 Windows x64 平台测试失败。
|
|
128
|
+
- GitHub repository 或 provenance 发布链未确定,或冻结包名在发布前被他人占用。
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# 架构候选与推荐路线
|
|
2
|
+
|
|
3
|
+
## 1. 评价标准
|
|
4
|
+
|
|
5
|
+
候选路线按以下优先级评价:
|
|
6
|
+
|
|
7
|
+
1. xAI 官方支持证据与服务条款风险。
|
|
8
|
+
2. 凭据暴露面和可审计性。
|
|
9
|
+
3. Harness LLM 流、reasoning 与工具调用的语义完整性。
|
|
10
|
+
4. macOS 与 Windows 一致性。
|
|
11
|
+
5. 依赖体积、供应链与受管市场可安装性。
|
|
12
|
+
6. 用户体验与维护成本。
|
|
13
|
+
|
|
14
|
+
## 2. 候选比较
|
|
15
|
+
|
|
16
|
+
| 路线 | 认证所有者 | Harness 语义 | 跨平台 | 主要风险 | 决策 |
|
|
17
|
+
|---|---|---:|---:|---|---|
|
|
18
|
+
| A. 插件自管 device OAuth + 固定 Proxy | 插件 | 高 | 高 | Client ID 授权、令牌存储、刷新状态机 | ADR-0005 拒绝进入 `0.1.0` |
|
|
19
|
+
| B. Harness 发起官方 CLI 登录 + 固定 Proxy | xAI CLI | 高 | 高 | 依赖官方 CLI;子进程生命周期;上游文件格式变化 | **采用** |
|
|
20
|
+
| C. `grok -p` Headless | xAI CLI | 低 | 高 | prompt 展平、每次进程开销、工具与会话语义不完整 | 拒绝 Provider 路线 |
|
|
21
|
+
| D. `grok agent stdio` ACP | xAI CLI | 低 | 高 | agent 套 agent,权限、工具、会话边界重复 | 拒绝 Provider 路线 |
|
|
22
|
+
| E. xAI Console API Key | 插件/用户 | 高 | 高 | 独立计费,未必提供 Grok Build 订阅模型 | 后续独立模式 |
|
|
23
|
+
|
|
24
|
+
## 3. 推荐路线 B(官方 CLI 单路径)
|
|
25
|
+
|
|
26
|
+
### 数据流
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Harness renderer
|
|
30
|
+
│ 仅状态 DTO / 模型选择
|
|
31
|
+
▼
|
|
32
|
+
Harness Host
|
|
33
|
+
├─ GrokAdapter
|
|
34
|
+
│ ├─ 动态账号模型目录
|
|
35
|
+
│ ├─ prepareCall generation
|
|
36
|
+
│ └─ Harness chunk 流
|
|
37
|
+
├─ OfficialSessionCredentialSource
|
|
38
|
+
│ └─ 只读 xAI 官方 auth.json
|
|
39
|
+
├─ OfficialGrokLoginBridge
|
|
40
|
+
│ └─ 仅 ctx.subprocess.spawn([<constrained grok>, login --oauth|logout])
|
|
41
|
+
├─ PinnedGrokTransport
|
|
42
|
+
│ └─ 仅 cli-chat-proxy.grok.com
|
|
43
|
+
└─ ProviderWireCodec
|
|
44
|
+
├─ ResponsesCodec(当前真实模型)
|
|
45
|
+
└─ 其他 Grok 声明 backend 只有在协议与真机门禁通过后启用
|
|
46
|
+
|
|
47
|
+
xAI 官方 Grok Build CLI
|
|
48
|
+
├─ 标准配置:打开系统浏览器、OAuth、loopback callback、凭据写入
|
|
49
|
+
└─ 用户/企业有效配置:也可能选择外部 auth command 或企业 OIDC
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 深模块边界
|
|
53
|
+
|
|
54
|
+
#### `GrokAdapter`
|
|
55
|
+
|
|
56
|
+
对 Harness 暴露一个小而稳定的 Provider 接口,内部隐藏认证文件、HTTP headers、SSE 方言和重试细节。它拥有:
|
|
57
|
+
|
|
58
|
+
- `providerInfo` 与 retry policy。
|
|
59
|
+
- 动态、认证 generation 隔离的账号模型目录;不从模型名猜能力。
|
|
60
|
+
- `prepareCall()` 的 generation 快照。
|
|
61
|
+
- 输入验证、工具 schema 转换和 Harness chunk 顺序。
|
|
62
|
+
|
|
63
|
+
#### `OfficialSessionCredentialSource`
|
|
64
|
+
|
|
65
|
+
它只提供“为一次 Host 请求取得当前 access token”的能力,不把文件路径、JSON、token 或 refresh token暴露给调用方。设计约束:
|
|
66
|
+
|
|
67
|
+
- 路径只来自 Host 的 OS home 或 Host 启动时已有的 `GROK_HOME` 环境,不接受 UI/RPC 参数。
|
|
68
|
+
- 文件必须是普通文件,大小不超过 64 KiB;拒绝 symlink、reparse point、目录和设备文件。
|
|
69
|
+
- JSON 严格解析,只读取官方身份 key;未知结构返回 `AUTH_FORMAT_UNSUPPORTED`。
|
|
70
|
+
- 只接受唯一、无歧义、与发布绑定 CLI 版本的 xAI 生产 OIDC schema 相符的记录:`auth_mode`、issuer、client ID、scope、expiry 与 token 都必须通过闭合关系;external、API key、web-login、企业 issuer、legacy scope 和多记录全部拒绝。
|
|
71
|
+
- 这些字段是官方 CLI 写入的本地未签名 metadata,只能作为失败关闭的兼容性筛选,不能作为 token 第一方来源的密码学证明。
|
|
72
|
+
- token 仅在 Host 内存短暂缓存,按文件元数据变化和 401 失效。
|
|
73
|
+
- 永不直接写入、执行 OAuth refresh grant、迁移或删除官方凭据;过期时可启动固定的官方 `grok models` 命令,由 CLI 在自己的信任边界内刷新并写回,插件随后重新校验 snapshot。
|
|
74
|
+
|
|
75
|
+
#### `OfficialGrokLoginBridge`
|
|
76
|
+
|
|
77
|
+
为 Web 设置页和 TUI 提供与 `dsh-codex` 类似的“在 Harness 内发起登录”体验,但不接管 OAuth:
|
|
78
|
+
|
|
79
|
+
- 从 Host 启动时冻结的绝对 `GROK_HOME` 派生候选:macOS 默认 `~/.grok/bin/grok`;Windows 默认 `%USERPROFILE%\\.grok\\bin\\grok.exe`。不使用 `GROK_BIN_DIR`、PATH、workspace,也不接受 RPC/UI 传路径。
|
|
80
|
+
- macOS 官方安装会使用 symlink;必须 `realpath` 后确认目标位于同一 `~/.grok` 根下、是当前用户拥有的普通可执行文件。Windows 拒绝 reparse point 和非普通文件。
|
|
81
|
+
- 只使用 Harness 公开 `ctx.subprocess.resolveExecutable()` 与 `ctx.subprocess.spawn()`;不直接 import `node:child_process`,也不把 `@deepseek-ai/dsh-subprocess-local` 打进插件。
|
|
82
|
+
- 先执行固定 argv `[constrainedExecutable, "--version"]`,在 10 秒和 16 KiB 输出上限内验证版本格式,并要求版本属于发布时冻结的有限精确 allowlist;未经测试的更高版本也失败关闭。
|
|
83
|
+
- 登录只允许 `[constrainedExecutable, "login", "--oauth"]`;退出只允许 `[constrainedExecutable, "logout"]`。不拼接字符串、不显式启动 shell、不接受额外参数。
|
|
84
|
+
- `cwd` 使用受控的 Grok home,不使用用户 workspace;stdin 关闭,stdout/stderr 分别限制 64 KiB。
|
|
85
|
+
- 同时只允许一个登录事务;默认 5 分钟超时,支持取消与插件卸载清理。终止通过 Harness seam 对整棵进程树执行,Windows 与 macOS 行为由 Runtime 实现并在真实设备验证。
|
|
86
|
+
- 官方 CLI 自己打开浏览器并监听 callback。插件只映射进程状态,绝不把原始输出、授权 URL、state、code 或 token 送入 renderer。
|
|
87
|
+
- `grok login --oauth` 成功退出后再读取凭据;进程退出码为 0 但凭据无效时仍判定失败。
|
|
88
|
+
- `--oauth` 只固定 loopback transport,并不会绕过 CLI 的 external provider、devbox、OIDC、系统 managed config 或 MDM。检测到已知的 external/enterprise 环境覆盖时应在 spawn 前失败;即便未检测到,官方 CLI 及其有效配置仍是用户管理的信任边界,登录后的凭据门禁是最终防线。
|
|
89
|
+
- 首版只支持 Harness Host 与浏览器处于同一 macOS/Windows 桌面会话;远程 Web/headless 不假装成功,也不把 Host 的登录误报成客户端登录。
|
|
90
|
+
|
|
91
|
+
公开 Host API 只接受闭合动作:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
type AuthAction = "status" | "login" | "cancel" | "logout"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
它不是通用命令执行器,也不是通用 URL opener。
|
|
98
|
+
|
|
99
|
+
#### `PinnedGrokTransport`
|
|
100
|
+
|
|
101
|
+
调用方传 endpoint ID,不传 URL:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
type EndpointId = "models" | "responses" | "chat-completions" | "messages"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
生产映射编译期固定到:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
models -> GET https://cli-chat-proxy.grok.com/v1/models
|
|
111
|
+
responses -> POST https://cli-chat-proxy.grok.com/v1/responses
|
|
112
|
+
chat-completions -> POST https://cli-chat-proxy.grok.com/v1/chat/completions
|
|
113
|
+
messages -> POST https://cli-chat-proxy.grok.com/v1/messages
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
只有目录返回的闭合 `api_backend` 才能选择对应 endpoint;调用方不能直接传 URL。2026-08-26 的 `grok-4.6` 与 `grok-4.5` 都声明 `responses`。若真实账号目录出现尚未通过 codec 与真机测试的 backend,则“支持全部模型”门禁失败并阻断发布,不能静默隐藏该模型。
|
|
117
|
+
|
|
118
|
+
Transport 独占 Authorization 注入,强制 `redirect: "error"`、HTTPS、固定 origin/path、超时、取消和响应上限。测试通过构造时注入本地 transport,不在生产配置中暴露 base URL。
|
|
119
|
+
|
|
120
|
+
#### `ProviderWireCodec`
|
|
121
|
+
|
|
122
|
+
负责按 backend 把经过 schema 验证的远端增量转换为 Harness。当前 Responses 流已真实观察到 `response.created`、reasoning summary、output item/content part、output text 和 `response.completed`;所有 event 都必须经过闭合状态机后才转换为:
|
|
123
|
+
|
|
124
|
+
- `block-start`
|
|
125
|
+
- `text-delta` / `reasoning-delta`
|
|
126
|
+
- `tool-call-delta`
|
|
127
|
+
- `block-end`
|
|
128
|
+
- `usage`
|
|
129
|
+
- `finish`
|
|
130
|
+
|
|
131
|
+
`usage` 必须在 `finish` 前,`finish` 后不得再输出;截断或空成功响应必须产生稳定错误,不得假装成功。
|
|
132
|
+
|
|
133
|
+
### 登录的两层保证
|
|
134
|
+
|
|
135
|
+
- 插件可保证:renderer 无通用命令能力;传给 Harness seam 的 argv、cwd、stdio、环境覆盖与期限闭合;该 seam 不解释 shell;取消/卸载终止受管进程树;不符合绑定生产 OIDC schema 的凭据不会被本插件发往固定 Proxy。
|
|
136
|
+
- 插件不能保证:官方 CLI 不依据受信配置执行内部 shell、外部 helper、devbox 或企业 OIDC;也不能从未签名 `auth.json` metadata 密码学证明 token 的签发者。
|
|
137
|
+
|
|
138
|
+
因此推荐路线的前提是接受“官方 Grok CLI + 它的有效配置”为本地信任边界。若产品要求端到端绝不运行 shell,当前上游没有可验证的 builtin-only/no-config 参数,本路线必须阻断,等待上游能力或改用获得 xAI 授权的自有 OAuth client。
|
|
139
|
+
|
|
140
|
+
## 4. 已拒绝的自管 OAuth 责任
|
|
141
|
+
|
|
142
|
+
自行 OAuth 会同时引入以下责任;ADR-0005 决定 `0.1.0` 不承担它们:
|
|
143
|
+
|
|
144
|
+
- 取得明确授权的公共 OAuth Client ID,而不是复制官方 CLI 或第三方 Client ID。
|
|
145
|
+
- RFC 8628 device code、固定 verification URI、polling、取消、过期和重放防御。
|
|
146
|
+
- Harness credential grant record 的明文落盘边界与未来 keychain provider 迁移。
|
|
147
|
+
- refresh token 轮换、并发 single-flight、注销和恢复。
|
|
148
|
+
- 需要自行承担跨平台安全存储、刷新与账户切换,而 Harness 的子进程 seam 只能解决官方 CLI 生命周期,不能替代这些认证责任。
|
|
149
|
+
|
|
150
|
+
当前单一路径把这些责任留给官方 CLI。ADR-0003 仅作为已被取代的历史设计保留,发布包不包含对应实现。
|
|
151
|
+
|
|
152
|
+
## 5. 为什么不用 Headless
|
|
153
|
+
|
|
154
|
+
xAI 官方把 Headless 描述为简单脚本集成;示例会把消息压成 prompt,并运行一个完整 Grok agent 进程。作为 Harness LLM Provider,它无法可靠保证:
|
|
155
|
+
|
|
156
|
+
- 系统、用户、助手消息的原始角色边界。
|
|
157
|
+
- Harness tool schema 与增量 tool calls。
|
|
158
|
+
- Harness 自己的权限 UI 是唯一工具授权来源。
|
|
159
|
+
- 同一流的 usage、finish、abort 和 retry 语义。
|
|
160
|
+
|
|
161
|
+
因此 Headless 适合单独的“调用 Grok agent”功能,不适合本项目的 LLM adapter。
|
|
162
|
+
|
|
163
|
+
## 6. 为什么不用 ACP
|
|
164
|
+
|
|
165
|
+
ACP 面向 IDE 与 agent client,包含自己的 session、tools、permission 和 MCP 生命周期。把 ACP agent 包进 Harness LLM Provider 会形成两层 agent loop,让用户难以判断哪一层执行工具、保存会话和做权限确认。首版保持模型层集成,不引入 ACP。
|
|
166
|
+
|
|
167
|
+
## 7. API Key 备选
|
|
168
|
+
|
|
169
|
+
xAI Console API Key 是更传统的官方 API 路线,但通常是独立 API 计费,并不能自动等价于 Grok Build 订阅额度或模型目录。若未来需要,应作为明确命名的第二种认证模式,并使用 `api.x.ai` 的官方 API 文档;不能在失败时静默从订阅会话切换到可能计费的 API Key。
|
|
170
|
+
|
|
171
|
+
## 8. 首个开发验证
|
|
172
|
+
|
|
173
|
+
方案获批后的第一项工作不是大规模编码,而是一个受控协议 spike:
|
|
174
|
+
|
|
175
|
+
1. 使用用户自己的官方 CLI 登录会话。
|
|
176
|
+
2. 验证在受支持的标准 Grok 配置下,从 Web/TUI 触发 `grok login --oauth` 后,macOS 与 Windows 都由官方 CLI 打开系统浏览器;取消、超时和卸载会终止并等待 Harness seam 可观察的受管进程树;Windows 不出现额外 shell/console 闪窗。
|
|
177
|
+
3. 分别验证标准配置、external auth、企业 OIDC、环境覆盖与多记录 auth 文件;只允许与绑定版本生产 OIDC schema 相符的候选进入 transport,并记录官方 CLI 仍可能在内部执行配置命令这一残余风险。
|
|
178
|
+
4. 对固定 Chat Proxy 验证多轮消息、流式文本、reasoning、工具 schema、增量 tool call、usage、finish、401 和中止。
|
|
179
|
+
5. 只记录脱敏后的字段名称、类型和状态,不保存 prompt、token、完整响应或账号信息。
|
|
180
|
+
6. 若工具调用不能无损映射,或凭据筛选不足以维持固定 Proxy 边界,停止实现并回到本 ADR;不得用 prompt 拼接或 ACP 偷换路线。
|