@kya-os/cli 1.8.1 → 2.0.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/dist/commands/build.js +2 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/check.js +3 -3
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/checkpoint-init.d.ts +1 -1
- package/dist/commands/checkpoint-init.d.ts.map +1 -1
- package/dist/commands/checkpoint-init.js +8 -41
- package/dist/commands/checkpoint-init.js.map +1 -1
- package/dist/commands/claim.js +2 -2
- package/dist/commands/claim.js.map +1 -1
- package/dist/commands/dco.d.ts +38 -1
- package/dist/commands/dco.d.ts.map +1 -1
- package/dist/commands/dco.js +180 -7
- package/dist/commands/dco.js.map +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +18 -24
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/env.js +6 -6
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/identity.js +3 -3
- package/dist/commands/identity.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +103 -65
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/register.d.ts +1 -1
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +35 -7
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/rotate.js +4 -4
- package/dist/commands/rotate.js.map +1 -1
- package/dist/commands/start.js +3 -3
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/verify.js +6 -6
- package/dist/commands/verify.js.map +1 -1
- package/dist/dco.d.ts +4 -0
- package/dist/dco.d.ts.map +1 -0
- package/dist/dco.js +21 -0
- package/dist/dco.js.map +1 -0
- package/dist/index.js +8 -13
- package/dist/utils/checkpoint-api.d.ts +1 -1
- package/dist/utils/checkpoint-api.js +1 -1
- package/dist/utils/checkpoint-env.d.ts +1 -1
- package/dist/utils/checkpoint-env.js +2 -2
- package/dist/utils/checkpoint-env.js.map +1 -1
- package/dist/utils/checkpoint-scaffold.d.ts +1 -1
- package/dist/utils/checkpoint-scaffold.js +1 -1
- package/dist/utils/cli-kit/browser.d.ts +24 -0
- package/dist/utils/cli-kit/browser.d.ts.map +1 -0
- package/dist/utils/cli-kit/browser.js +58 -0
- package/dist/utils/cli-kit/browser.js.map +1 -0
- package/dist/utils/cli-kit/claim-polling.d.ts +18 -0
- package/dist/utils/cli-kit/claim-polling.d.ts.map +1 -0
- package/dist/utils/{claim-polling.js → cli-kit/claim-polling.js} +19 -26
- package/dist/utils/cli-kit/claim-polling.js.map +1 -0
- package/dist/utils/cli-kit/diagnostics.d.ts +9 -0
- package/dist/utils/cli-kit/diagnostics.d.ts.map +1 -0
- package/dist/utils/cli-kit/diagnostics.js +11 -0
- package/dist/utils/cli-kit/diagnostics.js.map +1 -0
- package/dist/utils/cli-kit/fs-safe.d.ts +14 -0
- package/dist/utils/cli-kit/fs-safe.d.ts.map +1 -0
- package/dist/utils/cli-kit/fs-safe.js +20 -0
- package/dist/utils/cli-kit/fs-safe.js.map +1 -0
- package/dist/utils/cli-kit/index.d.ts +15 -0
- package/dist/utils/cli-kit/index.d.ts.map +1 -0
- package/dist/utils/cli-kit/index.js +10 -0
- package/dist/utils/cli-kit/index.js.map +1 -0
- package/dist/utils/cli-kit/json-file.d.ts +21 -0
- package/dist/utils/cli-kit/json-file.d.ts.map +1 -0
- package/dist/utils/cli-kit/json-file.js +51 -0
- package/dist/utils/cli-kit/json-file.js.map +1 -0
- package/dist/utils/cli-kit/marker-block.d.ts +26 -0
- package/dist/utils/cli-kit/marker-block.d.ts.map +1 -0
- package/dist/utils/cli-kit/marker-block.js +61 -0
- package/dist/utils/cli-kit/marker-block.js.map +1 -0
- package/dist/utils/cli-kit/paths.d.ts +15 -0
- package/dist/utils/cli-kit/paths.d.ts.map +1 -0
- package/dist/utils/cli-kit/paths.js +21 -0
- package/dist/utils/cli-kit/paths.js.map +1 -0
- package/dist/utils/cli-kit/secure-storage.d.ts +14 -0
- package/dist/utils/cli-kit/secure-storage.d.ts.map +1 -0
- package/dist/utils/cli-kit/secure-storage.js +20 -0
- package/dist/utils/cli-kit/secure-storage.js.map +1 -0
- package/dist/utils/cli-kit/step-report.d.ts +16 -0
- package/dist/utils/cli-kit/step-report.d.ts.map +1 -0
- package/dist/utils/cli-kit/step-report.js +36 -0
- package/dist/utils/cli-kit/step-report.js.map +1 -0
- package/dist/utils/dco/agent-metadata.d.ts +1 -1
- package/dist/utils/dco/agent-metadata.js +1 -1
- package/dist/utils/dco/agent-metadata.js.map +1 -1
- package/dist/utils/dco/claude-config.d.ts +18 -14
- package/dist/utils/dco/claude-config.d.ts.map +1 -1
- package/dist/utils/dco/claude-config.js +31 -97
- package/dist/utils/dco/claude-config.js.map +1 -1
- package/dist/utils/dco/dispatch.d.ts +59 -0
- package/dist/utils/dco/dispatch.d.ts.map +1 -0
- package/dist/utils/dco/dispatch.js +332 -0
- package/dist/utils/dco/dispatch.js.map +1 -0
- package/dist/utils/dco/git-config.d.ts.map +1 -1
- package/dist/utils/dco/git-config.js +5 -3
- package/dist/utils/dco/git-config.js.map +1 -1
- package/dist/utils/dco/github.d.ts +8 -0
- package/dist/utils/dco/github.d.ts.map +1 -1
- package/dist/utils/dco/github.js +11 -0
- package/dist/utils/dco/github.js.map +1 -1
- package/dist/utils/dco/paths.d.ts +1 -8
- package/dist/utils/dco/paths.d.ts.map +1 -1
- package/dist/utils/dco/paths.js +3 -12
- package/dist/utils/dco/paths.js.map +1 -1
- package/dist/utils/dco/render.d.ts +1 -1
- package/dist/utils/dco/render.d.ts.map +1 -1
- package/dist/utils/dco/render.js +4 -21
- package/dist/utils/dco/render.js.map +1 -1
- package/dist/utils/dco/resolve-agent.d.ts +54 -0
- package/dist/utils/dco/resolve-agent.d.ts.map +1 -0
- package/dist/utils/dco/resolve-agent.js +164 -0
- package/dist/utils/dco/resolve-agent.js.map +1 -0
- package/dist/utils/dco/setup.d.ts +10 -7
- package/dist/utils/dco/setup.d.ts.map +1 -1
- package/dist/utils/dco/setup.js +304 -203
- package/dist/utils/dco/setup.js.map +1 -1
- package/dist/utils/dco/ssh-key.d.ts +13 -0
- package/dist/utils/dco/ssh-key.d.ts.map +1 -1
- package/dist/utils/dco/ssh-key.js +21 -0
- package/dist/utils/dco/ssh-key.js.map +1 -1
- package/dist/utils/dco/status.d.ts +1 -1
- package/dist/utils/dco/status.d.ts.map +1 -1
- package/dist/utils/dco/status.js +3 -3
- package/dist/utils/dco/status.js.map +1 -1
- package/dist/utils/dco/verify.d.ts +114 -0
- package/dist/utils/dco/verify.d.ts.map +1 -0
- package/dist/utils/dco/verify.js +383 -0
- package/dist/utils/dco/verify.js.map +1 -0
- package/dist/utils/gitignore.d.ts +17 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +40 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/identity-heal.d.ts +16 -0
- package/dist/utils/identity-heal.d.ts.map +1 -0
- package/dist/utils/identity-heal.js +37 -0
- package/dist/utils/identity-heal.js.map +1 -0
- package/dist/utils/poll.d.ts +1 -1
- package/dist/utils/poll.js +1 -1
- package/package.json +4 -3
- package/dist/utils/claim-polling.d.ts +0 -21
- package/dist/utils/claim-polling.d.ts.map +0 -1
- package/dist/utils/claim-polling.js.map +0 -1
- package/dist/utils/dco/fs-safe.d.ts +0 -14
- package/dist/utils/dco/fs-safe.d.ts.map +0 -1
- package/dist/utils/dco/fs-safe.js +0 -20
- package/dist/utils/dco/fs-safe.js.map +0 -1
package/dist/utils/dco/setup.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
|
-
import { chmodSync, existsSync,
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
+
import { ensureSecureDir, writeSecureFile, } from "../cli-kit/index.js";
|
|
6
7
|
import { readAgentMetadata, writeAgentMetadata } from "./agent-metadata.js";
|
|
7
8
|
import { buildAttributionBlock, updateClaudeSettings, upsertClaudeMdBlock, } from "./claude-config.js";
|
|
8
9
|
import { detectSigningConflicts, getGitConfigValue, installTrailerHook, isGitRepo, setGitConfig, } from "./git-config.js";
|
|
9
10
|
import { GhNotInstalledError, GhScopeError, getGhUser, githubNoreplyEmail, isGhAuthenticated, isGhInstalled, listSigningKeys, refreshAuthScope, signingKeyAlreadyUploaded, uploadSigningKey, SCOPE_REFRESH_COMMAND, } from "./github.js";
|
|
10
11
|
import { isValidSlug, keysDir, signingKeyPath } from "./paths.js";
|
|
11
|
-
import { convertEd25519ToSshKey } from "./ssh-key.js";
|
|
12
|
+
import { convertEd25519ToSshKey, sshFingerprintFromPublicKeyLine, } from "./ssh-key.js";
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
function slugify(value) {
|
|
14
15
|
return value
|
|
@@ -23,11 +24,14 @@ function slugify(value) {
|
|
|
23
24
|
*/
|
|
24
25
|
function sanitizeIdentityValue(value) {
|
|
25
26
|
// eslint-disable-next-line no-control-regex
|
|
26
|
-
return value
|
|
27
|
+
return value
|
|
28
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
29
|
+
.replace(/\s+/g, " ")
|
|
30
|
+
.trim();
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* Distinguishes a missing identity from a present-but-unparseable one. The
|
|
30
|
-
* two need different advice: "run
|
|
34
|
+
* two need different advice: "run kya-os register" after a corrupt read would
|
|
31
35
|
* overwrite a potentially recoverable key with a brand new identity.
|
|
32
36
|
*/
|
|
33
37
|
function readIdentityFile(cwd) {
|
|
@@ -110,6 +114,35 @@ async function promptForAgentName() {
|
|
|
110
114
|
]);
|
|
111
115
|
return { name: name.trim(), slug };
|
|
112
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the human committer's name/email: prefer git config, and only fall
|
|
119
|
+
* back to the authenticated GitHub profile for whichever field git is
|
|
120
|
+
* missing (never both at once, so a configured name survives a missing
|
|
121
|
+
* email or vice versa). Shared by both identity-resolution paths in
|
|
122
|
+
* `runDcoSetup`: this half of Step 2 is identical whether the agent side
|
|
123
|
+
* came from `.mcpi/` or from an explicit agent slug.
|
|
124
|
+
*/
|
|
125
|
+
async function resolveHumanIdentity(cwd) {
|
|
126
|
+
const gitName = await getGitConfigValue("user.name", cwd);
|
|
127
|
+
const gitEmail = await getGitConfigValue("user.email", cwd);
|
|
128
|
+
let ghUser = null;
|
|
129
|
+
if ((!gitName || !gitEmail) &&
|
|
130
|
+
(await isGhInstalled()) &&
|
|
131
|
+
(await isGhAuthenticated())) {
|
|
132
|
+
ghUser = await getGhUser();
|
|
133
|
+
}
|
|
134
|
+
// Use || (not ??) so an explicitly-empty git value is treated as missing,
|
|
135
|
+
// consistent with the truthiness gate above that decides to fetch gh.
|
|
136
|
+
const resolvedName = gitName || (ghUser ? ghUser.name || ghUser.login : undefined);
|
|
137
|
+
const resolvedEmail = gitEmail || (ghUser ? githubNoreplyEmail(ghUser) : undefined);
|
|
138
|
+
if (!resolvedName || !resolvedEmail) {
|
|
139
|
+
throw new Error("Could not resolve your git identity. Set git config user.name and user.email, or authenticate gh.");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
name: sanitizeIdentityValue(resolvedName),
|
|
143
|
+
email: sanitizeIdentityValue(resolvedEmail),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
113
146
|
/**
|
|
114
147
|
* Orchestrates the full DCO/attribution setup. Steps run in order; the three
|
|
115
148
|
* foundation steps (preflight, identities, key material) abort the run when
|
|
@@ -160,217 +193,289 @@ export async function runDcoSetup(options = {}) {
|
|
|
160
193
|
...laterSteps,
|
|
161
194
|
]);
|
|
162
195
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
identityRead.status === "ok"
|
|
173
|
-
? ".mcpi/identity.json is missing required fields (did, privateKey). Inspect or restore the file before continuing."
|
|
174
|
-
: "No agent identity found in .mcpi/identity.json. Run `mcpi register` first, then re-run `mcpi dco setup`.";
|
|
175
|
-
}
|
|
176
|
-
else if (await isIdentityTrackedByGit(cwd)) {
|
|
177
|
-
preflightFailure =
|
|
178
|
-
".mcpi/identity.json is tracked by git. A committed identity may have been shipped by the repo author; refusing to configure signing with it. Remove it from git history and generate your own identity with `mcpi register`.";
|
|
179
|
-
}
|
|
180
|
-
if (preflightFailure || !identity?.privateKey || !identity?.did) {
|
|
181
|
-
steps.push({
|
|
182
|
-
id: "preflight",
|
|
183
|
-
title: "Preflight checks",
|
|
184
|
-
status: "failed",
|
|
185
|
-
detail: preflightFailure ??
|
|
186
|
-
"No agent identity found in .mcpi/identity.json. Run `mcpi register` first, then re-run `mcpi dco setup`.",
|
|
187
|
-
});
|
|
188
|
-
return abort([
|
|
189
|
-
{ id: "identities", title: "Resolve agent and human identity" },
|
|
190
|
-
{ id: "key-material", title: "Write SSH signing key" },
|
|
191
|
-
...laterSteps,
|
|
192
|
-
]);
|
|
193
|
-
}
|
|
194
|
-
steps.push({ id: "preflight", title: "Preflight checks", status: "done" });
|
|
195
|
-
// Step 2: resolve identities
|
|
196
|
+
// Steps 1-3 (preflight, identity resolution, key material) branch here and
|
|
197
|
+
// rejoin at Step 4: an explicit agent slug wires signing for an EXISTING
|
|
198
|
+
// registered agent and never reads, requires, or writes `.mcpi/` — the
|
|
199
|
+
// identity is derived from the slug alone (the same derivation
|
|
200
|
+
// `dco dispatch <slug>` uses) and the key must already exist on disk,
|
|
201
|
+
// never generated or converted here. `agent`/`human`/`keyMaterial`/
|
|
202
|
+
// `privateKeyPath`/`publicKeyPath` are populated by whichever branch runs;
|
|
203
|
+
// both either abort the whole run or leave all five set by the time
|
|
204
|
+
// Step 4 reads them.
|
|
196
205
|
let agent;
|
|
197
206
|
let human;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
throw new Error("Agent name/slug unknown. Run `mcpi register`, or set AGENT_NAME and AGENT_SLUG.");
|
|
218
|
-
}
|
|
219
|
-
// The interactive prompt validates its own input; values arriving from
|
|
220
|
-
// agent.json or the environment must pass the same shape check because
|
|
221
|
-
// the slug becomes a filename under the keys directory.
|
|
222
|
-
if (!isValidSlug(slug)) {
|
|
223
|
-
throw new Error(`Invalid agent slug "${slug}": must be lowercase letters, numbers, and dashes.`);
|
|
224
|
-
}
|
|
225
|
-
name = sanitizeIdentityValue(name);
|
|
226
|
-
const metadataUrl = boundMetadata?.url
|
|
227
|
-
? sanitizeIdentityValue(boundMetadata.url)
|
|
228
|
-
: undefined;
|
|
207
|
+
let keyMaterial;
|
|
208
|
+
let privateKeyPath;
|
|
209
|
+
let publicKeyPath;
|
|
210
|
+
if (options.agent) {
|
|
211
|
+
if (!isValidSlug(options.agent)) {
|
|
212
|
+
steps.push({
|
|
213
|
+
id: "preflight",
|
|
214
|
+
title: "Preflight checks",
|
|
215
|
+
status: "failed",
|
|
216
|
+
detail: `Invalid agent slug "${options.agent}": must be lowercase letters, numbers, and dashes.`,
|
|
217
|
+
});
|
|
218
|
+
return abort([
|
|
219
|
+
{ id: "identities", title: "Resolve agent and human identity" },
|
|
220
|
+
{ id: "key-material", title: "Write SSH signing key" },
|
|
221
|
+
...laterSteps,
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
steps.push({ id: "preflight", title: "Preflight checks", status: "done" });
|
|
225
|
+
const slug = options.agent;
|
|
229
226
|
agent = {
|
|
230
|
-
did:
|
|
231
|
-
name,
|
|
227
|
+
did: `did:web:knowthat.ai:agents:${slug}`,
|
|
228
|
+
name: slug,
|
|
232
229
|
slug,
|
|
233
|
-
url:
|
|
234
|
-
? metadataUrl
|
|
235
|
-
: `https://knowthat.ai/agents/${slug}`,
|
|
230
|
+
url: `https://knowthat.ai/agents/${slug}`,
|
|
236
231
|
email: `${slug}@agents.knowthat.ai`,
|
|
237
232
|
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
(await isGhInstalled()) &&
|
|
249
|
-
(await isGhAuthenticated())) {
|
|
250
|
-
ghUser = await getGhUser();
|
|
251
|
-
}
|
|
252
|
-
// Use || (not ??) so an explicitly-empty git value is treated as missing,
|
|
253
|
-
// consistent with the truthiness gate above that decides to fetch gh.
|
|
254
|
-
const resolvedName = gitName || (ghUser ? ghUser.name || ghUser.login : undefined);
|
|
255
|
-
const resolvedEmail = gitEmail || (ghUser ? githubNoreplyEmail(ghUser) : undefined);
|
|
256
|
-
if (!resolvedName || !resolvedEmail) {
|
|
257
|
-
throw new Error("Could not resolve your git identity. Set git config user.name and user.email, or authenticate gh.");
|
|
258
|
-
}
|
|
259
|
-
human = {
|
|
260
|
-
name: sanitizeIdentityValue(resolvedName),
|
|
261
|
-
email: sanitizeIdentityValue(resolvedEmail),
|
|
262
|
-
};
|
|
263
|
-
result.agent = agent;
|
|
264
|
-
result.human = human;
|
|
265
|
-
// Persist resolved metadata when the bound record was incomplete (prompted,
|
|
266
|
-
// from env, or unbound), so a later non-interactive run resolves the agent
|
|
267
|
-
// without prompting AND the did binding is written for future runs.
|
|
268
|
-
// Best-effort: a cache-write failure must not fail identity resolution.
|
|
269
|
-
if (!options.dryRun &&
|
|
270
|
-
(!boundMetadata?.name || !boundMetadata?.slug || !boundMetadata?.did)) {
|
|
271
|
-
try {
|
|
272
|
-
writeAgentMetadata(cwd, {
|
|
273
|
-
name: agent.name,
|
|
274
|
-
slug: agent.slug,
|
|
275
|
-
url: agent.url,
|
|
276
|
-
did: agent.did,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
result.metadataPersistWarning =
|
|
281
|
-
error instanceof Error ? error.message : String(error);
|
|
282
|
-
}
|
|
233
|
+
try {
|
|
234
|
+
human = await resolveHumanIdentity(cwd);
|
|
235
|
+
result.agent = agent;
|
|
236
|
+
result.human = human;
|
|
237
|
+
steps.push({
|
|
238
|
+
id: "identities",
|
|
239
|
+
title: "Resolve agent and human identity",
|
|
240
|
+
status: "done",
|
|
241
|
+
detail: `agent=${agent.name} (${agent.slug}), human=${human.name} <${human.email}>`,
|
|
242
|
+
});
|
|
283
243
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
]);
|
|
302
|
-
}
|
|
303
|
-
// Step 3: key material
|
|
304
|
-
let keyMaterial;
|
|
305
|
-
// Slug was validated in the identities step; signingKeyPath re-checks and
|
|
306
|
-
// always yields a path inside the keys directory.
|
|
307
|
-
const privateKeyPath = signingKeyPath(agent.slug);
|
|
308
|
-
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
309
|
-
// Only the write branch below sets this; a failure before it (e.g. a bad
|
|
310
|
-
// identity.json that fails conversion) must NOT delete pre-existing keys.
|
|
311
|
-
let startedKeyWrite = false;
|
|
312
|
-
try {
|
|
313
|
-
keyMaterial = convertEd25519ToSshKey(identity.privateKey, `mcpi:${agent.slug}`, { expectedPublicKeyBase64: identity.publicKey });
|
|
314
|
-
result.privateKeyPath = privateKeyPath;
|
|
315
|
-
result.fingerprint = keyMaterial.fingerprint;
|
|
316
|
-
// Compare BOTH files against the (now deterministic) expected material, so
|
|
317
|
-
// a corrupt/truncated private key with an intact .pub is detected and
|
|
318
|
-
// rewritten rather than left in place to fail signing forever.
|
|
319
|
-
const privUpToDate = existsSync(privateKeyPath) &&
|
|
320
|
-
readFileSync(privateKeyPath, "utf-8") === keyMaterial.privateKeyPem;
|
|
321
|
-
const pubUpToDate = existsSync(publicKeyPath) &&
|
|
322
|
-
readFileSync(publicKeyPath, "utf-8").trim() === keyMaterial.publicKeyLine;
|
|
323
|
-
if (privUpToDate && pubUpToDate) {
|
|
244
|
+
catch (error) {
|
|
245
|
+
steps.push({
|
|
246
|
+
id: "identities",
|
|
247
|
+
title: "Resolve agent and human identity",
|
|
248
|
+
status: "failed",
|
|
249
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
250
|
+
});
|
|
251
|
+
return abort([
|
|
252
|
+
{ id: "key-material", title: "Write SSH signing key" },
|
|
253
|
+
...laterSteps,
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
// Slug was validated above; signingKeyPath re-checks and always yields a
|
|
257
|
+
// path inside the keys directory.
|
|
258
|
+
privateKeyPath = signingKeyPath(agent.slug);
|
|
259
|
+
publicKeyPath = `${privateKeyPath}.pub`;
|
|
260
|
+
if (!existsSync(privateKeyPath) || !existsSync(publicKeyPath)) {
|
|
324
261
|
steps.push({
|
|
325
262
|
id: "key-material",
|
|
326
263
|
title: "Write SSH signing key",
|
|
327
|
-
status: "
|
|
328
|
-
detail: `
|
|
264
|
+
status: "failed",
|
|
265
|
+
detail: `No key for agent "${agent.slug}" at ${privateKeyPath}; run setup in the repo that registered it, or check the slug.`,
|
|
329
266
|
});
|
|
267
|
+
return abort(laterSteps);
|
|
330
268
|
}
|
|
331
|
-
|
|
269
|
+
try {
|
|
270
|
+
const publicKeyLine = readFileSync(publicKeyPath, "utf-8").trim();
|
|
271
|
+
const fingerprint = sshFingerprintFromPublicKeyLine(publicKeyLine);
|
|
272
|
+
keyMaterial = { publicKeyLine, fingerprint };
|
|
273
|
+
result.privateKeyPath = privateKeyPath;
|
|
274
|
+
result.fingerprint = fingerprint;
|
|
332
275
|
steps.push({
|
|
333
276
|
id: "key-material",
|
|
334
277
|
title: "Write SSH signing key",
|
|
335
|
-
status: "
|
|
336
|
-
detail: `
|
|
278
|
+
status: "done",
|
|
279
|
+
detail: `Using existing key at ${privateKeyPath} (${fingerprint})`,
|
|
337
280
|
});
|
|
338
281
|
}
|
|
339
|
-
|
|
340
|
-
mkdirSync(keysDir(), { recursive: true, mode: 0o700 });
|
|
341
|
-
startedKeyWrite = true;
|
|
342
|
-
// mode on write avoids any window where the key exists with loose
|
|
343
|
-
// permissions; the chmod covers overwrites of a pre-existing file,
|
|
344
|
-
// where the mode option is ignored.
|
|
345
|
-
writeFileSync(privateKeyPath, keyMaterial.privateKeyPem, { mode: 0o600 });
|
|
346
|
-
chmodSync(privateKeyPath, 0o600);
|
|
347
|
-
writeFileSync(publicKeyPath, keyMaterial.publicKeyLine + "\n", {
|
|
348
|
-
mode: 0o644,
|
|
349
|
-
});
|
|
350
|
-
chmodSync(publicKeyPath, 0o644);
|
|
282
|
+
catch (error) {
|
|
351
283
|
steps.push({
|
|
352
284
|
id: "key-material",
|
|
353
285
|
title: "Write SSH signing key",
|
|
354
|
-
status: "
|
|
355
|
-
detail:
|
|
286
|
+
status: "failed",
|
|
287
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
356
288
|
});
|
|
289
|
+
return abort(laterSteps);
|
|
357
290
|
}
|
|
358
291
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
292
|
+
else {
|
|
293
|
+
const identityRead = readIdentityFile(cwd);
|
|
294
|
+
const identity = identityRead.status === "ok" ? identityRead.identity : null;
|
|
295
|
+
let preflightFailure = null;
|
|
296
|
+
if (identityRead.status === "corrupt") {
|
|
297
|
+
preflightFailure =
|
|
298
|
+
".mcpi/identity.json exists but could not be parsed. Do NOT re-run `kya-os register` (it would overwrite the identity); inspect or restore the file first.";
|
|
299
|
+
}
|
|
300
|
+
else if (!identity?.privateKey || !identity?.did) {
|
|
301
|
+
preflightFailure =
|
|
302
|
+
identityRead.status === "ok"
|
|
303
|
+
? ".mcpi/identity.json is missing required fields (did, privateKey). Inspect or restore the file before continuing."
|
|
304
|
+
: "No agent identity found in .mcpi/identity.json. Run `kya-os register` first, then re-run `dco setup`.";
|
|
305
|
+
}
|
|
306
|
+
else if (await isIdentityTrackedByGit(cwd)) {
|
|
307
|
+
preflightFailure =
|
|
308
|
+
".mcpi/identity.json is tracked by git. A committed identity may have been shipped by the repo author; refusing to configure signing with it. Remove it from git history and generate your own identity with `kya-os register`.";
|
|
309
|
+
}
|
|
310
|
+
if (preflightFailure || !identity?.privateKey || !identity?.did) {
|
|
311
|
+
steps.push({
|
|
312
|
+
id: "preflight",
|
|
313
|
+
title: "Preflight checks",
|
|
314
|
+
status: "failed",
|
|
315
|
+
detail: preflightFailure ??
|
|
316
|
+
"No agent identity found in .mcpi/identity.json. Run `kya-os register` first, then re-run `dco setup`.",
|
|
317
|
+
});
|
|
318
|
+
return abort([
|
|
319
|
+
{ id: "identities", title: "Resolve agent and human identity" },
|
|
320
|
+
{ id: "key-material", title: "Write SSH signing key" },
|
|
321
|
+
...laterSteps,
|
|
322
|
+
]);
|
|
323
|
+
}
|
|
324
|
+
steps.push({ id: "preflight", title: "Preflight checks", status: "done" });
|
|
325
|
+
try {
|
|
326
|
+
const metadata = readAgentMetadata(cwd);
|
|
327
|
+
if (metadata?.did && metadata.did !== identity.did) {
|
|
328
|
+
throw new Error(`.mcpi/agent.json is for a different agent (${metadata.did}) than .mcpi/identity.json (${identity.did}). Fix or remove agent.json before continuing.`);
|
|
329
|
+
}
|
|
330
|
+
// Only trust agent.json for attribution when it is cryptographically bound
|
|
331
|
+
// to this identity (its did matches identity.json). Unbound metadata (no
|
|
332
|
+
// did, e.g. planted in an untrusted repo) is ignored so it cannot spoof the
|
|
333
|
+
// agent name/slug/url; the resolved values are then persisted with the real
|
|
334
|
+
// did, healing the binding for later runs.
|
|
335
|
+
const boundMetadata = metadata && metadata.did === identity.did ? metadata : null;
|
|
336
|
+
let name = boundMetadata?.name ?? process.env.AGENT_NAME;
|
|
337
|
+
let slug = boundMetadata?.slug ?? process.env.AGENT_SLUG;
|
|
338
|
+
if ((!name || !slug) && interactive && !options.dryRun) {
|
|
339
|
+
const prompted = await promptForAgentName();
|
|
340
|
+
name = name ?? prompted.name;
|
|
341
|
+
slug = slug ?? prompted.slug;
|
|
342
|
+
}
|
|
343
|
+
if (!name || !slug) {
|
|
344
|
+
throw new Error("Agent name/slug unknown. Run `kya-os register`, or set AGENT_NAME and AGENT_SLUG.");
|
|
345
|
+
}
|
|
346
|
+
// The interactive prompt validates its own input; values arriving from
|
|
347
|
+
// agent.json or the environment must pass the same shape check because
|
|
348
|
+
// the slug becomes a filename under the keys directory.
|
|
349
|
+
if (!isValidSlug(slug)) {
|
|
350
|
+
throw new Error(`Invalid agent slug "${slug}": must be lowercase letters, numbers, and dashes.`);
|
|
351
|
+
}
|
|
352
|
+
name = sanitizeIdentityValue(name);
|
|
353
|
+
const metadataUrl = boundMetadata?.url
|
|
354
|
+
? sanitizeIdentityValue(boundMetadata.url)
|
|
355
|
+
: undefined;
|
|
356
|
+
agent = {
|
|
357
|
+
did: identity.did,
|
|
358
|
+
name,
|
|
359
|
+
slug,
|
|
360
|
+
url: metadataUrl && isTrustedAgentUrl(metadataUrl)
|
|
361
|
+
? metadataUrl
|
|
362
|
+
: `https://knowthat.ai/agents/${slug}`,
|
|
363
|
+
email: `${slug}@agents.knowthat.ai`,
|
|
364
|
+
};
|
|
365
|
+
// Resolve the human name and email INDEPENDENTLY, each preferring the
|
|
366
|
+
// configured git value and only falling back to gh for the field that is
|
|
367
|
+
// missing. Resolving them together would let a missing email drop a
|
|
368
|
+
// configured name (or vice versa) in favor of the GitHub profile.
|
|
369
|
+
// Reading gh here is unrelated to the key upload, so it stays available
|
|
370
|
+
// even under --skip-github (which only suppresses the upload).
|
|
371
|
+
human = await resolveHumanIdentity(cwd);
|
|
372
|
+
result.agent = agent;
|
|
373
|
+
result.human = human;
|
|
374
|
+
// Persist resolved metadata when the bound record was incomplete (prompted,
|
|
375
|
+
// from env, or unbound), so a later non-interactive run resolves the agent
|
|
376
|
+
// without prompting AND the did binding is written for future runs.
|
|
377
|
+
// Best-effort: a cache-write failure must not fail identity resolution.
|
|
378
|
+
if (!options.dryRun &&
|
|
379
|
+
(!boundMetadata?.name || !boundMetadata?.slug || !boundMetadata?.did)) {
|
|
380
|
+
try {
|
|
381
|
+
writeAgentMetadata(cwd, {
|
|
382
|
+
name: agent.name,
|
|
383
|
+
slug: agent.slug,
|
|
384
|
+
url: agent.url,
|
|
385
|
+
did: agent.did,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
result.metadataPersistWarning =
|
|
390
|
+
error instanceof Error ? error.message : String(error);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
steps.push({
|
|
394
|
+
id: "identities",
|
|
395
|
+
title: "Resolve agent and human identity",
|
|
396
|
+
status: "done",
|
|
397
|
+
detail: `agent=${agent.name} (${agent.slug}), human=${human.name} <${human.email}>`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
steps.push({
|
|
402
|
+
id: "identities",
|
|
403
|
+
title: "Resolve agent and human identity",
|
|
404
|
+
status: "failed",
|
|
405
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
406
|
+
});
|
|
407
|
+
return abort([
|
|
408
|
+
{ id: "key-material", title: "Write SSH signing key" },
|
|
409
|
+
...laterSteps,
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
// Slug was validated in the identities step; signingKeyPath re-checks and
|
|
413
|
+
// always yields a path inside the keys directory.
|
|
414
|
+
privateKeyPath = signingKeyPath(agent.slug);
|
|
415
|
+
publicKeyPath = `${privateKeyPath}.pub`;
|
|
416
|
+
// Only the write branch below sets this; a failure before it (e.g. a bad
|
|
417
|
+
// identity.json that fails conversion) must NOT delete pre-existing keys.
|
|
418
|
+
let startedKeyWrite = false;
|
|
419
|
+
try {
|
|
420
|
+
const converted = convertEd25519ToSshKey(identity.privateKey, `mcpi:${agent.slug}`, { expectedPublicKeyBase64: identity.publicKey });
|
|
421
|
+
keyMaterial = converted;
|
|
422
|
+
result.privateKeyPath = privateKeyPath;
|
|
423
|
+
result.fingerprint = converted.fingerprint;
|
|
424
|
+
// Compare BOTH files against the (now deterministic) expected material, so
|
|
425
|
+
// a corrupt/truncated private key with an intact .pub is detected and
|
|
426
|
+
// rewritten rather than left in place to fail signing forever.
|
|
427
|
+
const privUpToDate = existsSync(privateKeyPath) &&
|
|
428
|
+
readFileSync(privateKeyPath, "utf-8") === converted.privateKeyPem;
|
|
429
|
+
const pubUpToDate = existsSync(publicKeyPath) &&
|
|
430
|
+
readFileSync(publicKeyPath, "utf-8").trim() === converted.publicKeyLine;
|
|
431
|
+
if (privUpToDate && pubUpToDate) {
|
|
432
|
+
steps.push({
|
|
433
|
+
id: "key-material",
|
|
434
|
+
title: "Write SSH signing key",
|
|
435
|
+
status: "done",
|
|
436
|
+
detail: `Already present at ${privateKeyPath}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else if (options.dryRun) {
|
|
440
|
+
steps.push({
|
|
441
|
+
id: "key-material",
|
|
442
|
+
title: "Write SSH signing key",
|
|
443
|
+
status: "would-change",
|
|
444
|
+
detail: `Would write ${privateKeyPath} and ${publicKeyPath}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
ensureSecureDir(keysDir());
|
|
449
|
+
startedKeyWrite = true;
|
|
450
|
+
writeSecureFile(privateKeyPath, converted.privateKeyPem);
|
|
451
|
+
writeFileSync(publicKeyPath, converted.publicKeyLine + "\n", {
|
|
452
|
+
mode: 0o644,
|
|
453
|
+
});
|
|
454
|
+
chmodSync(publicKeyPath, 0o644);
|
|
455
|
+
steps.push({
|
|
456
|
+
id: "key-material",
|
|
457
|
+
title: "Write SSH signing key",
|
|
458
|
+
status: "done",
|
|
459
|
+
detail: `${privateKeyPath} (${converted.fingerprint})`,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
// Clean up only a key pair THIS run started writing (a half-written pair);
|
|
465
|
+
// a failure before the write (e.g. bad identity.json) must leave any
|
|
466
|
+
// pre-existing valid keys intact so signing is not silently broken.
|
|
467
|
+
if (!options.dryRun && startedKeyWrite) {
|
|
468
|
+
rmSync(privateKeyPath, { force: true });
|
|
469
|
+
rmSync(publicKeyPath, { force: true });
|
|
470
|
+
}
|
|
471
|
+
steps.push({
|
|
472
|
+
id: "key-material",
|
|
473
|
+
title: "Write SSH signing key",
|
|
474
|
+
status: "failed",
|
|
475
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
476
|
+
});
|
|
477
|
+
return abort(laterSteps);
|
|
366
478
|
}
|
|
367
|
-
steps.push({
|
|
368
|
-
id: "key-material",
|
|
369
|
-
title: "Write SSH signing key",
|
|
370
|
-
status: "failed",
|
|
371
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
372
|
-
});
|
|
373
|
-
return abort(laterSteps);
|
|
374
479
|
}
|
|
375
480
|
// Step 4: GitHub upload
|
|
376
481
|
if (options.skipGithub) {
|
|
@@ -388,7 +493,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
388
493
|
id: "github-upload",
|
|
389
494
|
title: "Upload signing key to GitHub",
|
|
390
495
|
status: "action-required",
|
|
391
|
-
detail: "GitHub CLI not found. Install from https://cli.github.com, run `gh auth login`, then re-run `
|
|
496
|
+
detail: "GitHub CLI not found. Install from https://cli.github.com, run `gh auth login`, then re-run `dco setup`.",
|
|
392
497
|
});
|
|
393
498
|
}
|
|
394
499
|
else if (!(await isGhAuthenticated())) {
|
|
@@ -396,7 +501,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
396
501
|
id: "github-upload",
|
|
397
502
|
title: "Upload signing key to GitHub",
|
|
398
503
|
status: "action-required",
|
|
399
|
-
detail: "Not authenticated. Run `gh auth login`, then re-run `
|
|
504
|
+
detail: "Not authenticated. Run `gh auth login`, then re-run `dco setup`.",
|
|
400
505
|
});
|
|
401
506
|
}
|
|
402
507
|
else {
|
|
@@ -438,7 +543,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
438
543
|
id: "github-upload",
|
|
439
544
|
title: "Upload signing key to GitHub",
|
|
440
545
|
status: "action-required",
|
|
441
|
-
detail: "Upload declined. Re-run `
|
|
546
|
+
detail: "Upload declined. Re-run `dco setup` when you are ready to upload the key.",
|
|
442
547
|
});
|
|
443
548
|
}
|
|
444
549
|
else {
|
|
@@ -458,7 +563,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
458
563
|
id: "github-upload",
|
|
459
564
|
title: "Upload signing key to GitHub",
|
|
460
565
|
status: "action-required",
|
|
461
|
-
detail: `${error.message} Then re-run \`
|
|
566
|
+
detail: `${error.message} Then re-run \`dco setup\`.`,
|
|
462
567
|
});
|
|
463
568
|
}
|
|
464
569
|
else if (error instanceof GhNotInstalledError) {
|
|
@@ -626,13 +731,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
626
731
|
else {
|
|
627
732
|
try {
|
|
628
733
|
const block = buildAttributionBlock({
|
|
629
|
-
agentName: agent.name,
|
|
630
|
-
agentUrl: agent.url,
|
|
631
|
-
agentDid: agent.did,
|
|
632
734
|
includeTrailerInstructions: hookFallback,
|
|
633
|
-
humanName: human.name,
|
|
634
|
-
humanEmail: human.email,
|
|
635
|
-
agentEmail: agent.email,
|
|
636
735
|
});
|
|
637
736
|
const md = upsertClaudeMdBlock(cwd, block, { dryRun: options.dryRun });
|
|
638
737
|
const settings = updateClaudeSettings(cwd, { dryRun: options.dryRun });
|
|
@@ -681,7 +780,9 @@ export async function runDcoSetup(options = {}) {
|
|
|
681
780
|
"Trailer instructions would be added to the CLAUDE.md attribution block instead.";
|
|
682
781
|
}
|
|
683
782
|
else {
|
|
684
|
-
outcome = `Trailers were NOT added to CLAUDE.md (${options.skipClaude
|
|
783
|
+
outcome = `Trailers were NOT added to CLAUDE.md (${options.skipClaude
|
|
784
|
+
? "--skip-claude"
|
|
785
|
+
: "the Claude Code step did not complete"}); add Signed-off-by / Co-authored-by manually or re-run without --skip-claude.`;
|
|
685
786
|
}
|
|
686
787
|
if (trailerStep) {
|
|
687
788
|
trailerStep.detail = `${hookFallbackDetail} ${outcome}`;
|
|
@@ -702,7 +803,7 @@ export async function runDcoSetup(options = {}) {
|
|
|
702
803
|
const verifyDir = mkdtempSync(join(tmpdir(), "mcpi-dco-verify-"));
|
|
703
804
|
const payloadPath = join(verifyDir, "payload");
|
|
704
805
|
try {
|
|
705
|
-
writeFileSync(payloadPath, "
|
|
806
|
+
writeFileSync(payloadPath, "dco verification payload\n");
|
|
706
807
|
await execFileAsync("ssh-keygen", [
|
|
707
808
|
"-Y",
|
|
708
809
|
"sign",
|