opencode-harness-kit 0.3.7 → 0.3.8
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 +25 -1
- package/package.json +1 -1
- package/src/trust.js +77 -56
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,29 @@ is not happening.
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.3.8] — 2026-10-20
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`oc-harness trust` completes the two-factor challenge the way npm does, so
|
|
22
|
+
it works with whatever second factor the account has.** npm answers a
|
|
23
|
+
request that needs one with the URL to authenticate at and the URL to
|
|
24
|
+
collect the result from - the URL in the error text is masked, but the one
|
|
25
|
+
in the JSON body is not.
|
|
26
|
+
|
|
27
|
+
The URL is opened in a browser and printed, the result is collected, and the
|
|
28
|
+
command carries on. That works with an authenticator app, a security key,
|
|
29
|
+
or a code sent to the user's email - none of which is a code typed into a
|
|
30
|
+
terminal.
|
|
31
|
+
|
|
32
|
+
npm's own helper does the same thing, but only when stdin AND stdout are
|
|
33
|
+
both terminals. A tool that pipes npm's output takes the terminal away from
|
|
34
|
+
it, and npm then gives up and prints a URL nobody can open. That is what
|
|
35
|
+
was happening, and it is why the command looked like it wanted a code from
|
|
36
|
+
an authenticator app and nothing else.
|
|
37
|
+
- The wait for a human is configurable with `OC_TRUST_WAIT_SECONDS`, so a CI
|
|
38
|
+
run does not hang on it.
|
|
39
|
+
|
|
17
40
|
## [0.3.7] — 2026-10-20
|
|
18
41
|
|
|
19
42
|
### Fixed
|
|
@@ -335,7 +358,8 @@ silent in the worst way:
|
|
|
335
358
|
|
|
336
359
|
---
|
|
337
360
|
|
|
338
|
-
[Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.
|
|
361
|
+
[Unreleased]: https://github.com/pham-canh/space-harness/compare/v0.3.8...HEAD
|
|
362
|
+
[0.3.8]: https://github.com/pham-canh/space-harness/compare/v0.3.7...v0.3.8
|
|
339
363
|
[0.3.7]: https://github.com/pham-canh/space-harness/compare/v0.3.6...v0.3.7
|
|
340
364
|
[0.3.6]: https://github.com/pham-canh/space-harness/compare/v0.3.5...v0.3.6
|
|
341
365
|
[0.3.5]: https://github.com/pham-canh/space-harness/compare/v0.3.4...v0.3.5
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-harness-kit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Harness kit for OpenCode: capability profiles, a fleet of job lanes with one free model each, role-based reasoning effort, context pruning, and measured baselines.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/pham-canh/space-harness#readme",
|
package/src/trust.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// only runs from one directory is a release tool that people run from the
|
|
26
26
|
// wrong one.
|
|
27
27
|
|
|
28
|
-
import { execFileSync } from "node:child_process"
|
|
28
|
+
import { execFileSync, spawnSync } from "node:child_process"
|
|
29
29
|
import { readFileSync } from "node:fs"
|
|
30
30
|
import { dirname, join } from "node:path"
|
|
31
31
|
import { fileURLToPath } from "node:url"
|
|
@@ -84,80 +84,101 @@ function npm(args, opts = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Run npm
|
|
88
|
-
* it again.
|
|
87
|
+
* Run npm with its output captured.
|
|
89
88
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* @param {string[]} args
|
|
90
|
+
* @param {{ otp?: string }} [state]
|
|
91
|
+
* @returns {{ out: string, code: number }}
|
|
92
|
+
*/
|
|
93
|
+
function npmCaptured(args, state = {}) {
|
|
94
|
+
const extra = state.otp ? [`--otp=${state.otp}`] : []
|
|
95
|
+
const r = spawnSync("npm", [...args, ...extra], {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
})
|
|
99
|
+
return { out: `${r.stdout ?? ""}${r.stderr ?? ""}`, code: r.status ?? 1 }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run npm, and complete a two-factor challenge if it asks for one.
|
|
93
104
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
105
|
+
* npm answers a request that needs a second factor with a body carrying the
|
|
106
|
+
* URL to authenticate at and the URL to collect the result from. The URL in
|
|
107
|
+
* the ERROR TEXT is masked - `auth/cli/***` - but the one in the JSON body is
|
|
108
|
+
* not, and that is the one that can be opened.
|
|
109
|
+
*
|
|
110
|
+
* So the challenge is completed the way npm completes it: the URL is opened
|
|
111
|
+
* in a browser, the user answers there however their account is set up - a
|
|
112
|
+
* code from an authenticator app, a security key, or one mailed to them - and
|
|
113
|
+
* the result is collected from the second URL.
|
|
114
|
+
*
|
|
115
|
+
* npm's own helper does exactly this, but only when stdin AND stdout are
|
|
116
|
+
* both terminals. A tool that pipes npm's output takes the terminal away from
|
|
117
|
+
* it, and npm then gives up and prints a URL nobody can open. So the flow is
|
|
118
|
+
* done here, where the URL can be recovered.
|
|
96
119
|
*
|
|
97
120
|
* @param {string[]} args
|
|
98
121
|
* @param {{ otp?: string }} [state]
|
|
99
122
|
* @returns {Promise<{ out: string, code: number }>}
|
|
100
123
|
*/
|
|
101
124
|
async function npmWithOtp(args, state = {}) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
code: 0,
|
|
111
|
-
}
|
|
112
|
-
} catch (err) {
|
|
113
|
-
const e = /** @type {any} */ (err)
|
|
114
|
-
return { out: `${e.stdout ?? ""}${e.stderr ?? ""}`, code: e.status ?? 1 }
|
|
115
|
-
}
|
|
116
|
-
}
|
|
125
|
+
let result = npmCaptured(args, state)
|
|
126
|
+
if (result.code === 0) return result
|
|
127
|
+
|
|
128
|
+
const m = /"doneUrl"\s*:\s*"[^"]*authId=([a-f0-9-]+)"/.exec(result.out)
|
|
129
|
+
if (!m) return result
|
|
130
|
+
const authId = m[1]
|
|
131
|
+
const authUrl = `https://www.npmjs.com/auth/cli/${authId}`
|
|
132
|
+
const doneUrl = `https://registry.npmjs.org/-/v1/done?authId=${authId}`
|
|
117
133
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
// Open it if this machine can. The URL is printed either way, because a
|
|
135
|
+
// browser that cannot be opened here can be opened anywhere.
|
|
136
|
+
try {
|
|
137
|
+
execFileSync("xdg-open", [authUrl], { stdio: "ignore", timeout: 10_000 })
|
|
138
|
+
} catch {
|
|
139
|
+
// No browser here. The URL below is the way out.
|
|
140
|
+
}
|
|
141
|
+
console.error("")
|
|
142
|
+
console.error(bold(" npm needs to confirm it is you."))
|
|
143
|
+
console.error(` ${authUrl}`)
|
|
144
|
+
console.error(dim(" Answer there however your npm account is set up - an authenticator"))
|
|
145
|
+
console.error(dim(" app, a security key, or a code sent to your email."))
|
|
146
|
+
console.error(dim(" This page is open in a browser, or copy the URL above."))
|
|
147
|
+
console.error("")
|
|
121
148
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.error(dim(" run it again with --otp=<the code from your authenticator app>"))
|
|
127
|
-
return { out: result.out, code: result.code }
|
|
149
|
+
const token = await waitForToken(doneUrl)
|
|
150
|
+
if (!token) {
|
|
151
|
+
console.error(red(" no answer came back, so nothing was changed."))
|
|
152
|
+
return result
|
|
128
153
|
}
|
|
129
|
-
state.otp =
|
|
130
|
-
result =
|
|
154
|
+
state.otp = token
|
|
155
|
+
result = npmCaptured(args, state)
|
|
131
156
|
return result
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
/**
|
|
135
|
-
*
|
|
160
|
+
* Wait for the browser to answer.
|
|
136
161
|
*
|
|
162
|
+
* @param {string} doneUrl
|
|
137
163
|
* @returns {Promise<string | null>}
|
|
138
164
|
*/
|
|
139
|
-
async function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await rl.question(" one-time password from your authenticator app: ")
|
|
155
|
-
).trim()
|
|
156
|
-
process.stderr.write("\n")
|
|
157
|
-
return answer || null
|
|
158
|
-
} finally {
|
|
159
|
-
rl.close()
|
|
165
|
+
async function waitForToken(doneUrl) {
|
|
166
|
+
// How long to wait for a human. The window is generous, because the person
|
|
167
|
+
// may be answering an email, and it is configurable so a CI run does not
|
|
168
|
+
// hang on it.
|
|
169
|
+
const seconds = Number(process.env.OC_TRUST_WAIT_SECONDS ?? 600)
|
|
170
|
+
const attempts = Math.max(1, Math.round(seconds / 5))
|
|
171
|
+
for (let i = 0; i < attempts; i++) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(doneUrl)
|
|
174
|
+
const body = /** @type {any} */ (await res.json())
|
|
175
|
+
if (body && typeof body.token === "string" && body.token) return body.token
|
|
176
|
+
} catch {
|
|
177
|
+
// Not ready yet, or the network blipped. Try again.
|
|
178
|
+
}
|
|
179
|
+
await new Promise((r) => setTimeout(r, 5000))
|
|
160
180
|
}
|
|
181
|
+
return null
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
/**
|