@mnemom/mnemom 0.17.38-next.1 → 0.17.38-next.2
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/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +12 -6
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +7 -14
- package/dist/lib/runner/executor-client.d.ts +6 -0
- package/dist/lib/runner/executor-client.js +6 -0
- package/dist/lib/runner/job.d.ts +12 -3
- package/dist/lib/runner/job.js +30 -2
- package/dist/lib/scan/caps.d.ts +1 -35
- package/dist/lib/scan/caps.js +2 -67
- package/dist/lib/scan/cost-per-pr.d.ts +1 -93
- package/dist/lib/scan/cost-per-pr.js +2 -101
- package/dist/lib/scan/credential.d.ts +2 -26
- package/dist/lib/scan/credential.js +5 -58
- package/dist/lib/scan/failure.d.ts +1 -10
- package/dist/lib/scan/failure.js +2 -43
- package/dist/lib/scan/pr-source.d.ts +1 -57
- package/dist/lib/scan/pr-source.js +2 -132
- package/dist/lib/scan/report-email.d.ts +7 -104
- package/dist/lib/scan/report-email.js +25 -97
- package/dist/lib/scan/scan.d.ts +1 -69
- package/dist/lib/scan/scan.js +2 -195
- package/dist/lib/scan/share.d.ts +3 -1
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
- package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
- package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
- package/dist/lib/scan/spend/anthropic-usage.js +2 -269
- package/dist/lib/scan/spend/prices.d.ts +1 -33
- package/dist/lib/scan/spend/prices.js +2 -57
- package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
- package/dist/lib/scan/spend/spend-estimate.js +2 -70
- package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
- package/dist/lib/scan/spend/spend-tiers.js +6 -26
- package/dist/lib/scan/spend/token-rate.d.ts +1 -30
- package/dist/lib/scan/spend/token-rate.js +2 -54
- package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
- package/dist/vendor/shared/org-scan-core/caps.js +68 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
- package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
- package/dist/vendor/shared/org-scan-core/credential.js +74 -0
- package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
- package/dist/vendor/shared/org-scan-core/failure.js +43 -0
- package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
- package/dist/vendor/shared/org-scan-core/index.js +19 -0
- package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
- package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
- package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
- package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
- package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
- package/dist/vendor/shared/org-scan-core/scan.js +202 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
- package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
- package/package.json +1 -1
- /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
9
|
*
|
|
10
10
|
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
-
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv). Under
|
|
12
|
+
* --oidc the PR is pushed and opened with the job's PR token instead (ruling 34.34): the api's
|
|
13
|
+
* Record App token for the seal's repo, fetched with the lease key, carried the same way.
|
|
12
14
|
*
|
|
13
15
|
* So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
|
|
14
16
|
* ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
|
|
@@ -35,7 +37,9 @@ export interface RunDeps {
|
|
|
35
37
|
stop: AbortSignal;
|
|
36
38
|
/** The text of this host's executor-state.json, or null when there is none. */
|
|
37
39
|
readExecutorState?(): Promise<string | null>;
|
|
38
|
-
jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases, modelKey: string
|
|
40
|
+
jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases, modelKey: string,
|
|
41
|
+
/** True under --oidc: the job opens its PR with the api's PR token (ruling 34.34). */
|
|
42
|
+
prTokenFromLease: boolean): JobDeps;
|
|
39
43
|
}
|
|
40
44
|
/** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
|
|
41
45
|
export declare const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
|
|
@@ -78,6 +82,10 @@ export declare function liveJobDeps(api: RunnerApi, opts: {
|
|
|
78
82
|
executorId: string | null;
|
|
79
83
|
github: GithubBases;
|
|
80
84
|
modelKey: string;
|
|
85
|
+
/** The credential is the --oidc exchange: the PR uses the job's PR token (ruling 34.34). */
|
|
86
|
+
prTokenFromLease?: boolean;
|
|
87
|
+
/** The git runner for an env; tests inject one to see which credentials a push carries. */
|
|
88
|
+
makeGit?: (env: NodeJS.ProcessEnv) => Git;
|
|
81
89
|
}): JobDeps;
|
|
82
90
|
/** The process wiring: signals, stdout and stderr. Resolves the exit code. */
|
|
83
91
|
export declare function runAgentRun(flags: RunFlags): Promise<number>;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
9
|
*
|
|
10
10
|
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
-
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv). Under
|
|
12
|
+
* --oidc the PR is pushed and opened with the job's PR token instead (ruling 34.34): the api's
|
|
13
|
+
* Record App token for the seal's repo, fetched with the lease key, carried the same way.
|
|
12
14
|
*
|
|
13
15
|
* So is the Anthropic key (ruling 34.30.1): the phases' model calls are billed to the runner's own
|
|
14
16
|
* ANTHROPIC_API_KEY, which only the model proxy holds and sets on each upstream request. A runner
|
|
@@ -91,7 +93,7 @@ export async function agentRunCommand(flags, deps) {
|
|
|
91
93
|
}
|
|
92
94
|
const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
|
|
93
95
|
const loop = { failMode: plan.failMode, stop: deps.stop, emit: deps.emit };
|
|
94
|
-
const job = deps.jobDeps(api, executorId, gh.bases, modelKey);
|
|
96
|
+
const job = deps.jobDeps(api, executorId, gh.bases, modelKey, plan.credential.kind === "oidc");
|
|
95
97
|
return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
|
|
96
98
|
}
|
|
97
99
|
// ── GitHub and git, for the live driver ──────────────────────────────────────
|
|
@@ -259,7 +261,10 @@ export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.e
|
|
|
259
261
|
export function liveJobDeps(api, opts) {
|
|
260
262
|
const gh = opts.github;
|
|
261
263
|
const token = resolveGithubToken(opts.env);
|
|
262
|
-
const
|
|
264
|
+
const makeGit = opts.makeGit ?? liveGit;
|
|
265
|
+
const git = makeGit(gitAuthEnv(opts.env, token, gh.web));
|
|
266
|
+
// The PR token's git: a fresh env, so its header replaces the runner token's, never adds to it.
|
|
267
|
+
const gitFor = (t) => t === null ? git : makeGit(gitAuthEnv(opts.env, t, gh.web));
|
|
263
268
|
return {
|
|
264
269
|
api,
|
|
265
270
|
executorId: opts.executorId,
|
|
@@ -272,11 +277,12 @@ export function liveJobDeps(api, opts) {
|
|
|
272
277
|
startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state, opts.modelKey),
|
|
273
278
|
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
|
|
274
279
|
runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
|
|
275
|
-
openPr: (repo, ws, title, body) => openPr(
|
|
280
|
+
openPr: (repo, ws, title, body, prToken) => openPr(gitFor(prToken), opts.fetch, prToken ?? token, repo, ws, title, body, gh.api),
|
|
276
281
|
commitsAhead: (ws) => commitsAhead(git, ws),
|
|
277
282
|
headSha: (ws) => headSha(git, ws),
|
|
278
283
|
commitBodies: (ws, since, lineageKey, repo) => commitBodies(git, ws, since, lineageKey, repo),
|
|
279
|
-
enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
|
|
284
|
+
enableAutoMerge: (prUrl, prToken) => enableAutoMerge(opts.fetch, prToken ?? token, prUrl, gh),
|
|
285
|
+
prTokenFromLease: opts.prTokenFromLease === true,
|
|
280
286
|
cleanup: cleanupWorkspace,
|
|
281
287
|
};
|
|
282
288
|
}
|
|
@@ -296,7 +302,7 @@ export function runAgentRun(flags) {
|
|
|
296
302
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
297
303
|
stop: stop.signal,
|
|
298
304
|
readExecutorState: () => readFile(executorStatePath(homedir()), "utf8").catch(() => null),
|
|
299
|
-
jobDeps: (api, executorId, github, modelKey) => liveJobDeps(api, { fetch, env: process.env, executorId, github, modelKey }),
|
|
305
|
+
jobDeps: (api, executorId, github, modelKey, prTokenFromLease) => liveJobDeps(api, { fetch, env: process.env, executorId, github, modelKey, prTokenFromLease }),
|
|
300
306
|
}).finally(() => {
|
|
301
307
|
process.off("SIGTERM", onSignal);
|
|
302
308
|
process.off("SIGINT", onSignal);
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { selectSpend } from "../lib/scan/spend/spend-tiers.js";
|
|
|
10
10
|
import { type AnthropicUsageDeps, type MeasuredUsage } from "../lib/scan/spend/anthropic-usage.js";
|
|
11
11
|
import { type WifDeps } from "../lib/scan/spend/anthropic-wif.js";
|
|
12
12
|
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
13
|
-
import {
|
|
13
|
+
import type { CostSpend } from "../lib/scan/cost-per-pr.js";
|
|
14
14
|
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
15
15
|
import { type RepoListDeps } from "../lib/scan/repo-picker.js";
|
|
16
16
|
export interface ScanCommandOptions {
|
package/dist/commands/scan.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getAuthInfo } from "../lib/auth.js";
|
|
|
15
15
|
import { listMyOrgs } from "../lib/api.js";
|
|
16
16
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
17
17
|
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
18
|
-
import {
|
|
18
|
+
import { costSpendFor, sendsSpendRange } from "../vendor/shared/org-scan-core/report-build.js";
|
|
19
19
|
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
20
20
|
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
21
21
|
import { pickRepos, REPO_PICKER_NO_TERMINAL_HINT, } from "../lib/scan/repo-picker.js";
|
|
@@ -110,15 +110,8 @@ export async function resolveSelfReportedSpend(o) {
|
|
|
110
110
|
* tiers off) the modelled range. Sources are never mixed.
|
|
111
111
|
*/
|
|
112
112
|
export function resolveCostSpend(o, result, spend, measured) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (spend.tier === 1) {
|
|
116
|
-
const usd = measured?.usd ?? o.anthropicUsageUsd;
|
|
117
|
-
return typeof usd === "number" ? { source: "anthropic_usage", usd } : null;
|
|
118
|
-
}
|
|
119
|
-
if (spend.tier === 2)
|
|
120
|
-
return null;
|
|
121
|
-
return spend.tier === 3 ? modelledSpend(result.spend) : null;
|
|
113
|
+
// Shared with the Mnemom-side run (MNE-9752): same spend source, same cost figures.
|
|
114
|
+
return costSpendFor(result, spend, measured?.usd ?? o.anthropicUsageUsd);
|
|
122
115
|
}
|
|
123
116
|
export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
124
117
|
export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
@@ -353,21 +346,21 @@ export async function scanCommand(o) {
|
|
|
353
346
|
* returns it so the caller exits non-zero after the local report is shown.
|
|
354
347
|
*/
|
|
355
348
|
async function emailReport(o, org, result, level, spend, period, costSpend, extraEmails) {
|
|
356
|
-
const fail = (f) => {
|
|
357
|
-
console.log(failureMessage(f));
|
|
349
|
+
const fail = (f, retryAfterMs) => {
|
|
350
|
+
console.log(failureMessage(f, retryAfterMs));
|
|
358
351
|
return f;
|
|
359
352
|
};
|
|
360
353
|
const report = buildEmailReport(result, {
|
|
361
354
|
orgId: org,
|
|
362
355
|
now: o.now ?? Date.now,
|
|
363
|
-
sendSpendRange: spend
|
|
356
|
+
sendSpendRange: sendsSpendRange(spend),
|
|
364
357
|
period,
|
|
365
358
|
costSpend,
|
|
366
359
|
});
|
|
367
360
|
const body = buildEmailBody(report, level, result.coverage.complete, extraEmails);
|
|
368
361
|
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
|
369
362
|
if (outcome.status === "failed")
|
|
370
|
-
return fail(outcome.reason);
|
|
363
|
+
return fail(outcome.reason, outcome.retryAfterMs);
|
|
371
364
|
if (outcome.status === "unavailable") {
|
|
372
365
|
// Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
|
|
373
366
|
console.log(UNAVAILABLE_MSG);
|
|
@@ -34,6 +34,11 @@ export declare function errorDetails(body: unknown): Record<string, unknown>;
|
|
|
34
34
|
export declare const JOB_READ_PATH: (sessionId: string) => string;
|
|
35
35
|
/** The served bundle fetch, per claim (W-L12). */
|
|
36
36
|
export declare const BUNDLE_FETCH_PATH: (sessionId: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
39
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GITHUB_TOKEN_PATH: (sessionId: string) => string;
|
|
37
42
|
export declare const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
38
43
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
39
44
|
export declare const RECORD_COMMITS_PATH: (orgId: string) => string;
|
|
@@ -65,6 +70,7 @@ export interface RunnerApi {
|
|
|
65
70
|
fail(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
|
|
66
71
|
job(sessionId: string): Promise<HttpResult>;
|
|
67
72
|
bundle(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
73
|
+
githubToken(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
68
74
|
jwks(): Promise<HttpResult>;
|
|
69
75
|
/** One RecordCommit/v1 body, with the lease key; the api pins it to the session's lineage. */
|
|
70
76
|
recordCommit(orgId: string, body: RecordCommitBody, leaseKey: string): Promise<HttpResult>;
|
|
@@ -33,6 +33,11 @@ export function errorDetails(body) {
|
|
|
33
33
|
export const JOB_READ_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/job`;
|
|
34
34
|
/** The served bundle fetch, per claim (W-L12). */
|
|
35
35
|
export const BUNDLE_FETCH_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/bundle`;
|
|
36
|
+
/**
|
|
37
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
38
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
39
|
+
*/
|
|
40
|
+
export const GITHUB_TOKEN_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/github-token`;
|
|
36
41
|
export const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
37
42
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
38
43
|
export const RECORD_COMMITS_PATH = (orgId) => `/v1/orgs/${encodeURIComponent(orgId)}/record/commits`;
|
|
@@ -95,6 +100,7 @@ export function runnerApi(opts) {
|
|
|
95
100
|
fail: (id, body) => call(routePath("exec_fail", scoped(id)), "POST", ex, body),
|
|
96
101
|
job: (id) => call(JOB_READ_PATH(scoped(id)), "GET", ex),
|
|
97
102
|
bundle: (id, leaseKey) => call(BUNDLE_FETCH_PATH(scoped(id)), "GET", leaseKey),
|
|
103
|
+
githubToken: (id, leaseKey) => call(GITHUB_TOKEN_PATH(scoped(id)), "POST", leaseKey),
|
|
98
104
|
jwks: () => call(JWKS_PATH, "GET", null),
|
|
99
105
|
recordCommit: (orgId, body, leaseKey) => {
|
|
100
106
|
if (!ORG_ID_RE.test(orgId))
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -42,8 +42,11 @@ export interface JobDeps {
|
|
|
42
42
|
prepareWorkspace(repo: string, sessionId: string): Promise<Workspace>;
|
|
43
43
|
/** Run one phase's process; resolves its exit code. Rejects only when it could not start. */
|
|
44
44
|
runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Push the job branch and open its PR; resolves the PR URL. `token` is the job's PR token when
|
|
47
|
+
* `prTokenFromLease` is set; null means the runner's own GitHub token.
|
|
48
|
+
*/
|
|
49
|
+
openPr(repo: string, ws: Workspace, title: string, body: string, token: string | null): Promise<string>;
|
|
47
50
|
/** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
|
|
48
51
|
commitsAhead(ws: Workspace): Promise<number>;
|
|
49
52
|
/** The working copy's HEAD sha before a phase runs; null only for a clone with no commit. Throws otherwise. */
|
|
@@ -53,7 +56,13 @@ export interface JobDeps {
|
|
|
53
56
|
* first, under `lineageKey` and the job's repo. A commit that cannot be read is left out.
|
|
54
57
|
*/
|
|
55
58
|
commitBodies(ws: Workspace, since: string | null, lineageKey: string, repo: string): Promise<RecordCommitBody[]>;
|
|
56
|
-
enableAutoMerge(prUrl: string): Promise<void>;
|
|
59
|
+
enableAutoMerge(prUrl: string, token: string | null): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* True when the credential is the --oidc exchange: the PR is pushed and opened with the job's PR
|
|
62
|
+
* token from the api (ruling 34.34), never the runner's own GitHub token, which in a sealed repo's
|
|
63
|
+
* Action is a GITHUB_TOKEN the org forbids from opening PRs. A refused token fails the job.
|
|
64
|
+
*/
|
|
65
|
+
prTokenFromLease?: boolean;
|
|
57
66
|
cleanup(ws: Workspace): Promise<void>;
|
|
58
67
|
}
|
|
59
68
|
export interface JobOptions {
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -379,6 +379,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
381
|
if (opensPr) {
|
|
382
|
+
let prToken = null;
|
|
382
383
|
try {
|
|
383
384
|
// Checked before the push, so an empty diff never reaches GitHub's 422 (amendment 59).
|
|
384
385
|
if ((await deps.commitsAhead(ws)) === 0) {
|
|
@@ -386,7 +387,13 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
386
387
|
await reportFail(deps, sessionId, claim.epoch, "runner_failed", "pr_no_changes");
|
|
387
388
|
return { kind: "outcome", outcome: failedOutcome(facts, "pr_no_changes") };
|
|
388
389
|
}
|
|
389
|
-
|
|
390
|
+
if (deps.prTokenFromLease) {
|
|
391
|
+
const t = await fetchPrToken(deps, sessionId, claim.leaseKey, job.repo);
|
|
392
|
+
if (!t.ok)
|
|
393
|
+
throw new Error(t.detail);
|
|
394
|
+
prToken = t.token;
|
|
395
|
+
}
|
|
396
|
+
facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`, prBody(job), prToken);
|
|
390
397
|
}
|
|
391
398
|
catch (e) {
|
|
392
399
|
deps.log(`opening the PR failed: ${e.message}`);
|
|
@@ -395,7 +402,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
395
402
|
}
|
|
396
403
|
if (facts.merge_strategy === "merge_on_green") {
|
|
397
404
|
try {
|
|
398
|
-
await deps.enableAutoMerge(facts.pr_url);
|
|
405
|
+
await deps.enableAutoMerge(facts.pr_url, prToken);
|
|
399
406
|
}
|
|
400
407
|
catch (e) {
|
|
401
408
|
deps.log(`enabling auto-merge failed (the PR stays open): ${e.message}`);
|
|
@@ -469,6 +476,27 @@ export async function reportPhaseCommits(deps, report, p) {
|
|
|
469
476
|
deps.log(`commit report failed: ${e.message}`);
|
|
470
477
|
}
|
|
471
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The job's PR token (ruling 34.34), fetched once per claim with the lease key. It must name the
|
|
481
|
+
* job's repo; the detail never carries the token.
|
|
482
|
+
*/
|
|
483
|
+
async function fetchPrToken(deps, sessionId, leaseKey, repo) {
|
|
484
|
+
const res = await deps.api.githubToken(sessionId, leaseKey);
|
|
485
|
+
if (res.status !== 200) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
detail: `the PR token fetch answered ${res.code ?? res.networkError ?? res.status}`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const b = (res.body ?? {});
|
|
492
|
+
if (typeof b.token !== "string" || b.token === "") {
|
|
493
|
+
return { ok: false, detail: "the PR token fetch answered 200 with no token" };
|
|
494
|
+
}
|
|
495
|
+
if (typeof b.repository !== "string" || b.repository.toLowerCase() !== repo.toLowerCase()) {
|
|
496
|
+
return { ok: false, detail: `the PR token names another repo than the job's ${repo}` };
|
|
497
|
+
}
|
|
498
|
+
return { ok: true, token: b.token };
|
|
499
|
+
}
|
|
472
500
|
async function fetchBundle(deps, sessionId, claim) {
|
|
473
501
|
const [jwksRes, res] = await Promise.all([
|
|
474
502
|
deps.api.jwks(),
|
package/dist/lib/scan/caps.d.ts
CHANGED
|
@@ -1,35 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Coverage caps and sampling. Port of mnemom-api src/org-scan/scan/capped-scan.ts (PR #4208).
|
|
3
|
-
*
|
|
4
|
-
* Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
|
|
5
|
-
* PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
|
|
6
|
-
*/
|
|
7
|
-
export declare const COVERAGE_MONTHS = 3;
|
|
8
|
-
export interface ScanCaps {
|
|
9
|
-
maxPrs: number;
|
|
10
|
-
/** A PR with more additions+deletions than this is skipped, never read in full. */
|
|
11
|
-
maxPrSizeLines: number;
|
|
12
|
-
/** Upstream read calls. */
|
|
13
|
-
maxReads: number;
|
|
14
|
-
/** Wall-clock budget. */
|
|
15
|
-
maxMillis: number;
|
|
16
|
-
}
|
|
17
|
-
export declare const DEFAULT_SCAN_CAPS: ScanCaps;
|
|
18
|
-
export type StopReason = "pr-cap" | "read-cap" | "time-cap";
|
|
19
|
-
/** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
|
|
20
|
-
export declare function monthsBefore(end: string, months: number): string;
|
|
21
|
-
/** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
|
|
22
|
-
export declare function evenSample<T>(items: readonly T[], k: number): T[];
|
|
23
|
-
export declare function sanitiseCaps(c: Partial<ScanCaps>): ScanCaps;
|
|
24
|
-
/** Running read budget. Every call is told how many reads it may spend (`room`). */
|
|
25
|
-
export declare class Budget {
|
|
26
|
-
private caps;
|
|
27
|
-
private now;
|
|
28
|
-
private startedAt;
|
|
29
|
-
reads: number;
|
|
30
|
-
constructor(caps: ScanCaps, now: () => number, startedAt: number);
|
|
31
|
-
block(n?: number): StopReason | null;
|
|
32
|
-
room(): number;
|
|
33
|
-
overrun(): StopReason | null;
|
|
34
|
-
charge(n: number): void;
|
|
35
|
-
}
|
|
1
|
+
export * from "../../vendor/shared/org-scan-core/caps.js";
|
package/dist/lib/scan/caps.js
CHANGED
|
@@ -1,67 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
|
|
5
|
-
* PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
|
|
6
|
-
*/
|
|
7
|
-
export const COVERAGE_MONTHS = 3;
|
|
8
|
-
export const DEFAULT_SCAN_CAPS = {
|
|
9
|
-
maxPrs: 2_000,
|
|
10
|
-
maxPrSizeLines: 20_000,
|
|
11
|
-
maxReads: 4_500,
|
|
12
|
-
maxMillis: 15 * 60_000,
|
|
13
|
-
};
|
|
14
|
-
/** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
|
|
15
|
-
export function monthsBefore(end, months) {
|
|
16
|
-
const [y, m, d] = end.split("-").map(Number);
|
|
17
|
-
const target = new Date(Date.UTC(y, m - 1 - months, 1));
|
|
18
|
-
const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
|
|
19
|
-
target.setUTCDate(Math.min(d, last));
|
|
20
|
-
return target.toISOString().slice(0, 10);
|
|
21
|
-
}
|
|
22
|
-
/** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
|
|
23
|
-
export function evenSample(items, k) {
|
|
24
|
-
const n = items.length;
|
|
25
|
-
if (n <= k)
|
|
26
|
-
return [...items];
|
|
27
|
-
return Array.from({ length: k }, (_, i) => items[Math.floor((i * n) / k)]);
|
|
28
|
-
}
|
|
29
|
-
export function sanitiseCaps(c) {
|
|
30
|
-
const pos = (n, d) => typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : d;
|
|
31
|
-
return {
|
|
32
|
-
maxPrs: pos(c.maxPrs, DEFAULT_SCAN_CAPS.maxPrs),
|
|
33
|
-
maxPrSizeLines: pos(c.maxPrSizeLines, DEFAULT_SCAN_CAPS.maxPrSizeLines),
|
|
34
|
-
maxReads: pos(c.maxReads, DEFAULT_SCAN_CAPS.maxReads),
|
|
35
|
-
maxMillis: pos(c.maxMillis, DEFAULT_SCAN_CAPS.maxMillis),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
/** Running read budget. Every call is told how many reads it may spend (`room`). */
|
|
39
|
-
export class Budget {
|
|
40
|
-
caps;
|
|
41
|
-
now;
|
|
42
|
-
startedAt;
|
|
43
|
-
reads = 0;
|
|
44
|
-
constructor(caps, now, startedAt) {
|
|
45
|
-
this.caps = caps;
|
|
46
|
-
this.now = now;
|
|
47
|
-
this.startedAt = startedAt;
|
|
48
|
-
}
|
|
49
|
-
block(n = 1) {
|
|
50
|
-
if (this.now() - this.startedAt >= this.caps.maxMillis)
|
|
51
|
-
return "time-cap";
|
|
52
|
-
if (this.reads + n > this.caps.maxReads)
|
|
53
|
-
return "read-cap";
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
room() {
|
|
57
|
-
if (this.now() - this.startedAt >= this.caps.maxMillis)
|
|
58
|
-
return 0;
|
|
59
|
-
return Math.max(0, this.caps.maxReads - this.reads);
|
|
60
|
-
}
|
|
61
|
-
overrun() {
|
|
62
|
-
return this.reads > this.caps.maxReads ? "read-cap" : null;
|
|
63
|
-
}
|
|
64
|
-
charge(n) {
|
|
65
|
-
this.reads += n;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../vendor/shared/org-scan-core/caps.js";
|
|
@@ -1,93 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Cost per merged PR and cost by PR size, for the Org Scan report. Pure: numbers in, numbers out.
|
|
3
|
-
*
|
|
4
|
-
* Source of the spend. Measured per-request cost cannot be tied to a PR with the data the scan has:
|
|
5
|
-
* the Anthropic usage reads return one whole-org total (no per-request or per-PR rows) and the Mnemom
|
|
6
|
-
* Record totals are one window sum (model cost and a request count, no PR key). So every figure here is
|
|
7
|
-
* `label: "estimate"`: the period's spend split across the merged PRs in proportion to lines changed.
|
|
8
|
-
* Each figure names the spend it was split from (`spend_source`); sources are never mixed in one report.
|
|
9
|
-
*
|
|
10
|
-
* anthropic_usage the whole-org Anthropic cost for the period (the CTO's admin key)
|
|
11
|
-
* modelled_range our modelled estimate (a low to high range, so the figures are ranges)
|
|
12
|
-
*
|
|
13
|
-
* Size is per PR, in lines changed = additions + deletions, exactly as GitHub reports them. Lockfiles and
|
|
14
|
-
* generated files are NOT excluded. small < 50; medium 50..500 inclusive; large > 500.
|
|
15
|
-
*
|
|
16
|
-
* Each size figure is the average cost of ONE PR of that size: spend * (mean lines of the bucket) /
|
|
17
|
-
* (total lines of the PRs in scope). Scope: the modelled estimate prices AI-assisted PRs only, so it is
|
|
18
|
-
* split across those only (`pr_scope: "ai_assisted"`, buckets too); the others use every merged PR
|
|
19
|
-
* (`pr_scope: "all_merged"`). `pr_count` counts PRs in that scope. Usage-key figures also carry
|
|
20
|
-
* `spend_scope: "whole_org"` (the key covers the whole organization, not just the scanned repos). The cost per merged PR is the spend over the count of merged PRs.
|
|
21
|
-
*
|
|
22
|
-
* Missing data is an absent field, never 0 or NaN: no usable spend, zero merged PRs in the period, an
|
|
23
|
-
* empty bucket, a scan that did not read every merged PR in the period, or a result past safe-integer
|
|
24
|
-
* range. Reverts are listed without sizes, so they are not counted as merged PRs here.
|
|
25
|
-
*/
|
|
26
|
-
import type { SpendEstimate } from "./spend/spend-estimate.js";
|
|
27
|
-
/** Strictly below this is small. 50 is medium. */
|
|
28
|
-
export declare const SMALL_MAX_EXCLUSIVE = 50;
|
|
29
|
-
/** At or below this (and not small) is medium. 500 is medium; 501 is large. */
|
|
30
|
-
export declare const MEDIUM_MAX_INCLUSIVE = 500;
|
|
31
|
-
export type SizeBucket = "small" | "medium" | "large";
|
|
32
|
-
export type CostSpendSourceName = "anthropic_usage" | "modelled_range";
|
|
33
|
-
export interface PrSize {
|
|
34
|
-
/** YYYY-MM-DD, UTC. */
|
|
35
|
-
mergedDay: string;
|
|
36
|
-
/** additions + deletions. */
|
|
37
|
-
lines: number;
|
|
38
|
-
/** The PR carries an AI-tool mark (the same rule the modelled spend uses to count AI-assisted PRs). */
|
|
39
|
-
aiAssisted: boolean;
|
|
40
|
-
}
|
|
41
|
-
/** Where the spend comes from. USD, never cents. */
|
|
42
|
-
export type CostSpend = {
|
|
43
|
-
source: "anthropic_usage";
|
|
44
|
-
usd: number;
|
|
45
|
-
} | {
|
|
46
|
-
source: "modelled_range";
|
|
47
|
-
lowUsd: number;
|
|
48
|
-
highUsd: number;
|
|
49
|
-
};
|
|
50
|
-
/** One figure: a single value, or a low to high range for the modelled source. Integer cents. */
|
|
51
|
-
export type PrScope = "ai_assisted" | "all_merged";
|
|
52
|
-
export type CostFigure = {
|
|
53
|
-
/** "measured" is not possible with current data (see header); every figure is an estimate. */
|
|
54
|
-
label: "estimate";
|
|
55
|
-
spend_source: CostSpendSourceName;
|
|
56
|
-
/**
|
|
57
|
-
* Which PRs the average is over. The modelled estimate prices AI-assisted PRs only, so it is split
|
|
58
|
-
* across those only ("ai_assisted"); the usage key covers all spend, so they use "all_merged".
|
|
59
|
-
*/
|
|
60
|
-
pr_scope: PrScope;
|
|
61
|
-
/** PRs in that scope. */
|
|
62
|
-
pr_count: number;
|
|
63
|
-
/** Present on anthropic_usage only: the key's usage is the whole organization's, not just these PRs'. */
|
|
64
|
-
spend_scope?: "whole_org";
|
|
65
|
-
} & ({
|
|
66
|
-
value_cents: number;
|
|
67
|
-
} | {
|
|
68
|
-
low_cents: number;
|
|
69
|
-
high_cents: number;
|
|
70
|
-
});
|
|
71
|
-
export interface CostPerPr {
|
|
72
|
-
cost_per_merged_pr?: CostFigure;
|
|
73
|
-
cost_by_pr_size?: Partial<Record<SizeBucket, CostFigure>>;
|
|
74
|
-
}
|
|
75
|
-
export declare const sizeBucket: (lines: number) => SizeBucket;
|
|
76
|
-
/** Inclusive day count of YYYY-MM-DD..YYYY-MM-DD, or null if unparsable or reversed. */
|
|
77
|
-
export declare function periodDays(p: {
|
|
78
|
-
start: string;
|
|
79
|
-
end: string;
|
|
80
|
-
}): number | null;
|
|
81
|
-
/** The period's spend in USD as [low, high]; null when there is no usable spend. */
|
|
82
|
-
export declare function spendForPeriod(spend: CostSpend): [number, number] | null;
|
|
83
|
-
/** The modelled estimate as a spend source, or null when the estimate is "none". */
|
|
84
|
-
export declare const modelledSpend: (e: SpendEstimate) => CostSpend | null;
|
|
85
|
-
/**
|
|
86
|
-
* @param prs every merged PR the scan read, with merge day and size
|
|
87
|
-
* @param exhaustive true only if the scan read EVERY merged PR in the period (no sample, no skipped
|
|
88
|
-
* large PR, none unreadable, no cap hit). Otherwise the average is not defined.
|
|
89
|
-
*/
|
|
90
|
-
export declare function buildCostPerPr(prs: readonly PrSize[] | undefined, period: {
|
|
91
|
-
start: string;
|
|
92
|
-
end: string;
|
|
93
|
-
}, spend: CostSpend | null, exhaustive: boolean): CostPerPr;
|
|
1
|
+
export * from "../../vendor/shared/org-scan-core/cost-per-pr.js";
|
|
@@ -1,101 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
/** At or below this (and not small) is medium. 500 is medium; 501 is large. */
|
|
4
|
-
export const MEDIUM_MAX_INCLUSIVE = 500;
|
|
5
|
-
export const sizeBucket = (lines) => lines < SMALL_MAX_EXCLUSIVE ? "small" : lines <= MEDIUM_MAX_INCLUSIVE ? "medium" : "large";
|
|
6
|
-
const DAY_MS = 86_400_000;
|
|
7
|
-
/** Inclusive day count of YYYY-MM-DD..YYYY-MM-DD, or null if unparsable or reversed. */
|
|
8
|
-
export function periodDays(p) {
|
|
9
|
-
const s = Date.parse(`${p.start}T00:00:00Z`);
|
|
10
|
-
const e = Date.parse(`${p.end}T00:00:00Z`);
|
|
11
|
-
if (!Number.isFinite(s) || !Number.isFinite(e) || e < s)
|
|
12
|
-
return null;
|
|
13
|
-
return Math.round((e - s) / DAY_MS) + 1;
|
|
14
|
-
}
|
|
15
|
-
const pos = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
16
|
-
/** The period's spend in USD as [low, high]; null when there is no usable spend. */
|
|
17
|
-
export function spendForPeriod(spend) {
|
|
18
|
-
let r;
|
|
19
|
-
if (spend.source === "anthropic_usage") {
|
|
20
|
-
if (!pos(spend.usd))
|
|
21
|
-
return null;
|
|
22
|
-
r = [spend.usd, spend.usd];
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
if (!pos(spend.highUsd) || !(spend.lowUsd >= 0) || spend.lowUsd > spend.highUsd)
|
|
26
|
-
return null;
|
|
27
|
-
r = [spend.lowUsd, spend.highUsd];
|
|
28
|
-
}
|
|
29
|
-
return r.every((x) => Number.isFinite(x) && x * 100 <= Number.MAX_SAFE_INTEGER) ? r : null;
|
|
30
|
-
}
|
|
31
|
-
/** The modelled estimate as a spend source, or null when the estimate is "none". */
|
|
32
|
-
export const modelledSpend = (e) => e.kind === "range" ? { source: "modelled_range", lowUsd: e.lowUsd, highUsd: e.highUsd } : null;
|
|
33
|
-
/**
|
|
34
|
-
* `share` is the fraction of the period's spend one average PR here carries: always in (0, 1] by
|
|
35
|
-
* construction, and the spend is already bounded to safe-integer cents by spendForPeriod, so the
|
|
36
|
-
* rounded cents are finite safe integers without a further guard.
|
|
37
|
-
*/
|
|
38
|
-
function figure(spend, [low, high], count, share, scope) {
|
|
39
|
-
const c = (usd) => Math.round(usd * share * 100);
|
|
40
|
-
const base = {
|
|
41
|
-
label: "estimate",
|
|
42
|
-
spend_source: spend.source,
|
|
43
|
-
pr_scope: scope,
|
|
44
|
-
pr_count: count,
|
|
45
|
-
...(spend.source === "anthropic_usage" ? { spend_scope: "whole_org" } : {}),
|
|
46
|
-
};
|
|
47
|
-
return spend.source === "modelled_range"
|
|
48
|
-
? { ...base, low_cents: c(low), high_cents: c(high) }
|
|
49
|
-
: { ...base, value_cents: c(low) };
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* @param prs every merged PR the scan read, with merge day and size
|
|
53
|
-
* @param exhaustive true only if the scan read EVERY merged PR in the period (no sample, no skipped
|
|
54
|
-
* large PR, none unreadable, no cap hit). Otherwise the average is not defined.
|
|
55
|
-
*/
|
|
56
|
-
export function buildCostPerPr(prs, period, spend, exhaustive) {
|
|
57
|
-
if (!exhaustive || !spend || !Array.isArray(prs))
|
|
58
|
-
return {};
|
|
59
|
-
if (periodDays(period) === null)
|
|
60
|
-
return {};
|
|
61
|
-
const total = spendForPeriod(spend);
|
|
62
|
-
if (!total)
|
|
63
|
-
return {};
|
|
64
|
-
const inPeriod = prs.filter((p) => p.mergedDay >= period.start &&
|
|
65
|
-
p.mergedDay <= period.end &&
|
|
66
|
-
Number.isSafeInteger(p.lines) &&
|
|
67
|
-
p.lines >= 0);
|
|
68
|
-
// The modelled estimate is AI-tool spend, so it is split across AI-assisted PRs only; the usage key is
|
|
69
|
-
// the whole spend, so it is split across every merged PR.
|
|
70
|
-
const scope = spend.source === "modelled_range" ? "ai_assisted" : "all_merged";
|
|
71
|
-
const scoped = scope === "ai_assisted" ? inPeriod.filter((p) => p.aiAssisted) : inPeriod;
|
|
72
|
-
const n = scoped.length;
|
|
73
|
-
if (n === 0)
|
|
74
|
-
return {};
|
|
75
|
-
const out = {};
|
|
76
|
-
out.cost_per_merged_pr = figure(spend, total, n, 1 / n, scope);
|
|
77
|
-
let allLines = 0;
|
|
78
|
-
const agg = {
|
|
79
|
-
small: { n: 0, lines: 0 },
|
|
80
|
-
medium: { n: 0, lines: 0 },
|
|
81
|
-
large: { n: 0, lines: 0 },
|
|
82
|
-
};
|
|
83
|
-
for (const p of scoped) {
|
|
84
|
-
allLines += p.lines;
|
|
85
|
-
const a = agg[sizeBucket(p.lines)];
|
|
86
|
-
a.n += 1;
|
|
87
|
-
a.lines += p.lines;
|
|
88
|
-
}
|
|
89
|
-
// Splitting by lines needs lines: with none (or a sum past safe range) there is no size figure.
|
|
90
|
-
if (allLines <= 0 || !Number.isSafeInteger(allLines))
|
|
91
|
-
return out;
|
|
92
|
-
const bySize = {};
|
|
93
|
-
for (const b of ["small", "medium", "large"]) {
|
|
94
|
-
const a = agg[b];
|
|
95
|
-
if (a.n === 0)
|
|
96
|
-
continue;
|
|
97
|
-
bySize[b] = figure(spend, total, a.n, a.lines / a.n / allLines, scope);
|
|
98
|
-
}
|
|
99
|
-
out.cost_by_pr_size = bySize;
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
1
|
+
// Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
|
|
2
|
+
export * from "../../vendor/shared/org-scan-core/cost-per-pr.js";
|
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export interface GitHubCredential {
|
|
4
|
-
/** Fixed label for diagnostics ("app", "personal"): never a token, login or URL. */
|
|
5
|
-
readonly kind: string;
|
|
6
|
-
/** A bearer token for reading `repo` (owner/name). A rejection fails that read closed. */
|
|
7
|
-
tokenFor(repo: string): Promise<string>;
|
|
8
|
-
/**
|
|
9
|
-
* The permissions GitHub granted this credential, when the source can tell. Null = unknown (for
|
|
10
|
-
* example a classic token); the scan then relies on GET-only egress and does not claim more.
|
|
11
|
-
*/
|
|
12
|
-
grantedPermissions?(): Promise<Record<string, string> | null>;
|
|
13
|
-
}
|
|
14
|
-
/** Violations (empty when the map is exactly the read-only scan set). Port of readOnlyViolations. */
|
|
15
|
-
export declare function readOnlyViolations(permissions: Record<string, string> | null | undefined): string[];
|
|
16
|
-
/** owner/name, no dot-only segment. */
|
|
17
|
-
export declare function isValidRepoSlug(s: unknown): s is string;
|
|
18
|
-
/** Fixed-text failure: the message never carries a token, a body or a URL. */
|
|
19
|
-
export declare class ScanCredentialError extends Error {
|
|
20
|
-
constructor(message: "scan credential refused" | "scan credential not configured");
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Refuses a credential GitHub granted more than read to. Throws a fixed-text error and nothing else.
|
|
24
|
-
* Unknown (null) permissions pass: the source cannot tell, and the scan is GET-only regardless.
|
|
25
|
-
*/
|
|
26
|
-
export declare function assertReadOnlyCredential(c: GitHubCredential): Promise<void>;
|
|
1
|
+
import { type GitHubCredential } from "../../vendor/shared/org-scan-core/credential.js";
|
|
2
|
+
export * from "../../vendor/shared/org-scan-core/credential.js";
|
|
27
3
|
/** Runs `gh auth token`; resolves stdout or rejects. Injectable so tests never spawn a process. */
|
|
28
4
|
export type GhTokenRunner = () => Promise<string>;
|
|
29
5
|
/**
|