@sweny-ai/core 0.1.169 → 0.1.171
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/agent-env.d.ts +2 -1
- package/dist/agent-env.js +9 -3
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -0
- package/dist/budget.js +5 -1
- package/dist/cli/comment-output.d.ts +4 -2
- package/dist/cli/comment-output.js +16 -8
- package/dist/cli/main.js +42 -26
- package/dist/cli/new.js +3 -0
- package/dist/cli/output.d.ts +3 -2
- package/dist/cli/output.js +39 -37
- package/dist/cli/progress.d.ts +33 -0
- package/dist/cli/progress.js +69 -0
- package/dist/cli/renderer.d.ts +4 -0
- package/dist/cli/renderer.js +20 -13
- package/dist/cli/run-output.d.ts +13 -6
- package/dist/cli/run-output.js +36 -26
- package/dist/cli/runs.js +17 -11
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.js +48 -0
- package/dist/cli/terminal.d.ts +47 -0
- package/dist/cli/terminal.js +123 -0
- package/dist/cli/ticket.d.ts +89 -0
- package/dist/cli/ticket.js +218 -0
- package/dist/cli/try.d.ts +15 -8
- package/dist/cli/try.js +84 -65
- package/dist/executor.d.ts +8 -1
- package/dist/executor.js +21 -2
- package/dist/git-credentials.d.ts +106 -0
- package/dist/git-credentials.js +322 -0
- package/dist/harness/acp.js +3 -0
- package/dist/harness/claude-code.d.ts +7 -0
- package/dist/harness/claude-code.js +44 -3
- package/dist/harness/codex.js +3 -0
- package/dist/harness/pi.js +3 -0
- package/dist/harness/policy.d.ts +5 -0
- package/dist/harness/policy.js +14 -0
- package/dist/harness/sandbox-wrapper.d.ts +9 -2
- package/dist/harness/sandbox-wrapper.js +14 -4
- package/dist/harness/types.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mermaid.d.ts +10 -5
- package/dist/mermaid.js +11 -13
- package/dist/safe-outputs.js +5 -0
- package/dist/skills/git-push.d.ts +62 -0
- package/dist/skills/git-push.js +132 -0
- package/dist/skills/github.d.ts +17 -0
- package/dist/skills/github.js +23 -0
- package/dist/testing.d.ts +16 -12
- package/dist/testing.js +45 -20
- package/dist/theme.d.ts +185 -0
- package/dist/theme.js +183 -0
- package/dist/types.d.ts +2 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -1
- package/dist/workflows/triage.yml +2 -1
- package/package.json +6 -1
package/dist/cli/try.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `sweny try`: a zero-credential demo (#475).
|
|
3
3
|
*
|
|
4
4
|
* Replays a recorded explain-repo run through the REAL executor, with a replay
|
|
5
|
-
* harness standing in for the agent, so the real progress
|
|
6
|
-
* (#460), receipt, policy
|
|
5
|
+
* harness standing in for the agent, so the real progress lines, answer block
|
|
6
|
+
* (#460), receipt ticket, policy stamp and PR comment (#396, #479) are what a
|
|
7
7
|
* user sees. No agent, no model, no network, no env credentials, no files
|
|
8
8
|
* written (no `.sweny/runs`, no journal). The environment handed to the
|
|
9
9
|
* executor is empty by construction.
|
|
@@ -14,15 +14,19 @@
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import chalk from "chalk";
|
|
18
17
|
import { parse as parseYaml } from "yaml";
|
|
19
18
|
import { execute } from "../executor.js";
|
|
20
19
|
import { validateParsed } from "../loader.js";
|
|
21
20
|
import { WORKFLOW_TEMPLATES } from "./templates.js";
|
|
22
21
|
import { formatFinalOutput, resolveFinalOutput } from "./final-output.js";
|
|
23
22
|
import { formatRunComment } from "./comment-output.js";
|
|
24
|
-
import { formatReceiptDuration,
|
|
25
|
-
import {
|
|
23
|
+
import { formatReceiptDuration, summarizeRun } from "./run-output.js";
|
|
24
|
+
import { DagRenderer } from "./renderer.js";
|
|
25
|
+
import { createNodeProgress, toolCallsText } from "./progress.js";
|
|
26
|
+
import { createPaint } from "./style.js";
|
|
27
|
+
import { colorEnabled, glyphsFor, supportsUnicode, terminalColumns } from "./terminal.js";
|
|
28
|
+
import { writeReceipt } from "./ticket.js";
|
|
29
|
+
import { SWENY_TAGLINE } from "../theme.js";
|
|
26
30
|
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
27
31
|
/** Shape check with a message naming the first bad field. Throws on a malformed fixture. */
|
|
28
32
|
export function parseTryFixture(raw) {
|
|
@@ -152,34 +156,24 @@ export function createReplayHarness(fixture, opts) {
|
|
|
152
156
|
}
|
|
153
157
|
// ── Output ──────────────────────────────────────────────────────
|
|
154
158
|
export const TRY_BANNER_TITLE = "Recorded demo";
|
|
155
|
-
/** The
|
|
156
|
-
export const
|
|
157
|
-
"npm install -g @sweny-ai/core",
|
|
158
|
-
"sweny new --template explain-repo --yes",
|
|
159
|
-
"sweny workflow run .sweny/workflows/explain-repo.yml",
|
|
160
|
-
];
|
|
159
|
+
/** The one command to run next: scaffolds the same workflow on the user's repo and says how to run it. */
|
|
160
|
+
export const TRY_NEXT_COMMAND = "npx @sweny-ai/core new --template explain-repo";
|
|
161
161
|
/** Appended to `sweny try --help`. */
|
|
162
162
|
export const TRY_HELP_NOTE = "The recording is ILLUSTRATIVE SAMPLE DATA (a fictional repo, hand-written timings and token counts), not output from a real run. " +
|
|
163
163
|
"Nothing runs: no agent, no network, no credentials read, no files written.";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
chalk.dim(fixture.illustrative
|
|
169
|
-
? " The sample data is illustrative, not output from your code. No agent, no network, no credentials."
|
|
170
|
-
: " No agent, no network, no credentials."),
|
|
171
|
-
];
|
|
172
|
-
return lines.map((l, i) => (i === 0 ? ` ${l}` : l)).join("\n");
|
|
173
|
-
}
|
|
174
|
-
export function formatTryNext() {
|
|
164
|
+
/** The tagline, then the demo notice as one dim line. */
|
|
165
|
+
export function formatTryBanner(fixture, paint = createPaint(false), unicode = true) {
|
|
166
|
+
const g = glyphsFor(unicode);
|
|
167
|
+
const data = fixture.illustrative ? ", illustrative data" : "";
|
|
175
168
|
return [
|
|
176
|
-
` ${
|
|
177
|
-
|
|
178
|
-
...TRY_NEXT_COMMANDS.map((cmd) => ` ${cmd}`),
|
|
179
|
-
"",
|
|
180
|
-
chalk.dim(" Needs a Claude login (`claude` signed in) or ANTHROPIC_API_KEY. Your run ends with the same receipt."),
|
|
169
|
+
` ${paint.brand(g.brand)} ${paint.strong("SWEny")} ${SWENY_TAGLINE}`,
|
|
170
|
+
paint.muted(` ${TRY_BANNER_TITLE} of ${fixture.workflow} on a sample repo (${fixture.sample_repo})${data}. No agent, network or credentials.`),
|
|
181
171
|
].join("\n");
|
|
182
172
|
}
|
|
173
|
+
/** The closing line: one command. */
|
|
174
|
+
export function formatTryNext(paint = createPaint(false)) {
|
|
175
|
+
return ` ${paint.muted("Run it on your repo:")} ${paint.strong(TRY_NEXT_COMMAND)}`;
|
|
176
|
+
}
|
|
183
177
|
export const DEFAULT_TRY_PACE_MS = 1200;
|
|
184
178
|
const silentLogger = { info() { }, warn() { }, error() { }, debug() { } };
|
|
185
179
|
/** Replay the recording. Returns the process exit code. */
|
|
@@ -193,72 +187,97 @@ export async function runTry(opts = {}) {
|
|
|
193
187
|
if (!workflow.nodes[id])
|
|
194
188
|
throw new Error(`try-fixture.json: node "${id}" is not in ${fixture.workflow}`);
|
|
195
189
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
190
|
+
const env = process.env;
|
|
191
|
+
const unicode = opts.unicode ?? supportsUnicode(env);
|
|
192
|
+
const color = tty && colorEnabled({ isTTY: true }, env);
|
|
193
|
+
const paint = createPaint(color);
|
|
194
|
+
const g = glyphsFor(unicode);
|
|
195
|
+
const columns = opts.columns ?? terminalColumns(process.stdout, env);
|
|
196
|
+
write(`\n${formatTryBanner(fixture, paint, unicode)}\n\n`);
|
|
197
|
+
const progress = createNodeProgress({
|
|
198
|
+
write,
|
|
199
|
+
live: tty,
|
|
200
|
+
unicode,
|
|
201
|
+
paint,
|
|
202
|
+
idWidth: Math.max(...Object.keys(fixture.nodes).map((id) => id.length)),
|
|
203
|
+
});
|
|
204
|
+
// The same events drive the DAG in the comment preview.
|
|
205
|
+
const dag = new DagRenderer(workflow, { legend: false, unicode });
|
|
198
206
|
const observer = (event) => {
|
|
207
|
+
dag.update(event);
|
|
199
208
|
switch (event.type) {
|
|
200
|
-
case "workflow:start":
|
|
201
|
-
write(` ${c.brand("▲")} ${chalk.bold(event.workflow)}\n\n`);
|
|
202
|
-
break;
|
|
203
209
|
case "node:enter":
|
|
204
|
-
|
|
205
|
-
if (tty)
|
|
206
|
-
write(` ${c.subtle("○")} ${chalk.dim(event.node)}…`);
|
|
210
|
+
progress.enter(event.node);
|
|
207
211
|
break;
|
|
208
|
-
case "node:progress":
|
|
209
|
-
|
|
210
|
-
ticks.set(event.node, n);
|
|
211
|
-
if (tty) {
|
|
212
|
-
write(`\r\x1B[2K ${c.subtle("○")} ${chalk.dim(event.node)}… ${chalk.dim(`${n} tool ${n === 1 ? "call" : "calls"}`)}`);
|
|
213
|
-
}
|
|
212
|
+
case "node:progress":
|
|
213
|
+
progress.tick(event.node);
|
|
214
214
|
break;
|
|
215
|
-
}
|
|
216
215
|
case "node:exit": {
|
|
217
216
|
const rec = fixture.nodes[event.node];
|
|
218
|
-
const ok = event.result.status === "success";
|
|
219
|
-
const icon = ok ? c.ok("✓") : c.fail("✗");
|
|
220
217
|
const calls = event.result.toolCalls.length;
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
// The recording's timing, not the replay's wall clock.
|
|
219
|
+
const detail = [formatReceiptDuration(rec?.duration_ms ?? 0), ...(calls > 0 ? [toolCallsText(calls)] : [])];
|
|
220
|
+
const status = event.result.status === "success" ? "success" : event.result.status === "failed" ? "failed" : "skipped";
|
|
221
|
+
progress.exit(event.node, status, detail.join(` ${g.sep} `));
|
|
223
222
|
break;
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
225
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
226
|
+
let executed;
|
|
227
|
+
try {
|
|
228
|
+
executed = await execute(workflow, {}, {
|
|
229
|
+
skills: new Map(),
|
|
230
|
+
harness: createReplayHarness(fixture, { paceMs }),
|
|
231
|
+
observer,
|
|
232
|
+
logger: silentLogger,
|
|
233
|
+
// Nothing for the executor to read: no credentials, no PATH, no config.
|
|
234
|
+
env: {},
|
|
235
|
+
offline: true,
|
|
236
|
+
harnessPolicy: "warn",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
progress.stop();
|
|
241
|
+
}
|
|
242
|
+
const { results, trace } = executed;
|
|
237
243
|
write("\n");
|
|
238
|
-
// The recording's timings, not the replay's wall clock.
|
|
239
244
|
const durationsMs = Object.fromEntries(Object.entries(fixture.nodes).map(([id, n]) => [id, n.duration_ms]));
|
|
240
245
|
const totalMs = Object.values(durationsMs).reduce((a, b) => a + b, 0);
|
|
241
246
|
const receipt = summarizeRun(results, totalMs, false, trace);
|
|
242
247
|
const answer = resolveFinalOutput(workflow, results);
|
|
243
248
|
if (answer)
|
|
244
249
|
write(`${formatFinalOutput(answer)}\n\n`);
|
|
245
|
-
|
|
250
|
+
// Compact preview of the PR comment: its heading and DAG here, its ticket (the receipt) below.
|
|
246
251
|
const comment = formatRunComment(workflow, results, receipt, { trace, durationsMs });
|
|
247
|
-
write(` ${
|
|
248
|
-
|
|
252
|
+
write(` ${paint.muted("On a pull request, CI posts this heading, this DAG and the receipt below:")}\n\n`);
|
|
253
|
+
const tone = receipt.ok ? paint.success : paint.error;
|
|
254
|
+
write(` ${tone(receipt.ok ? g.success : g.failure)} ${paint.strong(workflow.name)}\n\n`);
|
|
255
|
+
write(`${dag
|
|
256
|
+
.renderToString()
|
|
249
257
|
.split("\n")
|
|
250
|
-
.
|
|
251
|
-
.
|
|
258
|
+
.slice(2)
|
|
259
|
+
.map((l) => (l === "" ? l : ` ${l}`))
|
|
260
|
+
.join("\n")}\n\n`);
|
|
261
|
+
await writeReceipt(receipt, {
|
|
262
|
+
workflow: workflow.id,
|
|
263
|
+
runId: "demo",
|
|
264
|
+
stream: { write, isTTY: tty, columns },
|
|
265
|
+
env,
|
|
266
|
+
rich: tty,
|
|
267
|
+
color,
|
|
268
|
+
unicode,
|
|
269
|
+
animate: !opts.fast,
|
|
270
|
+
});
|
|
252
271
|
if (opts.commentFile) {
|
|
253
272
|
try {
|
|
254
273
|
fs.mkdirSync(path.dirname(path.resolve(opts.commentFile)), { recursive: true });
|
|
255
274
|
fs.writeFileSync(opts.commentFile, comment);
|
|
256
275
|
}
|
|
257
276
|
catch (err) {
|
|
258
|
-
process.stderr.write(`
|
|
277
|
+
process.stderr.write(` ${g.warning} could not write comment file: ${err instanceof Error ? err.message : err}\n`);
|
|
259
278
|
}
|
|
260
279
|
}
|
|
261
|
-
write(`${formatTryNext()}\n\n`);
|
|
280
|
+
write(`${formatTryNext(paint)}\n\n`);
|
|
262
281
|
return receipt.ok ? 0 : 1;
|
|
263
282
|
}
|
|
264
283
|
export function registerTryCommand(program) {
|
|
@@ -274,7 +293,7 @@ export function registerTryCommand(program) {
|
|
|
274
293
|
if (options.pace !== undefined) {
|
|
275
294
|
paceMs = Number(options.pace);
|
|
276
295
|
if (!Number.isFinite(paceMs) || paceMs < 0) {
|
|
277
|
-
console.error(
|
|
296
|
+
console.error(createPaint(colorEnabled(process.stderr)).error(`\n --pace must be a non-negative number of milliseconds, got "${options.pace}"\n`));
|
|
278
297
|
process.exitCode = 1;
|
|
279
298
|
return;
|
|
280
299
|
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* This replaces ~8k lines of engine + recipe step code.
|
|
14
14
|
*/
|
|
15
|
-
import type { Workflow, Skill, Tool, Claude, Observer, Logger, ExecutionResult } from "./types.js";
|
|
15
|
+
import type { Workflow, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult } from "./types.js";
|
|
16
|
+
import "./skills/git-push.js";
|
|
16
17
|
import type { HarnessPolicyMode } from "./harness/policy.js";
|
|
17
18
|
import type { AgentHarness } from "./harness/types.js";
|
|
18
19
|
import type { DeciderMode } from "./decider.js";
|
|
@@ -138,6 +139,12 @@ export declare class RouteEvaluationError extends Error {
|
|
|
138
139
|
readonly node: string;
|
|
139
140
|
constructor(node: string, message: string);
|
|
140
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The bounded prompt context for one node: `input` plus its dependencies' entries.
|
|
144
|
+
* Exported for the property tests only; not part of the package API (index.ts does not re-export it).
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export declare function buildBoundedContext(workflow: Workflow, nodeId: string, results: Map<string, NodeResult>, input: unknown, instruction: string): Record<string, unknown>;
|
|
141
148
|
/**
|
|
142
149
|
* Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
|
|
143
150
|
* with no `access` is treated as a write so new tools fail safe.
|
package/dist/executor.js
CHANGED
|
@@ -22,6 +22,9 @@ import { resolveExecutionModel } from "./model.js";
|
|
|
22
22
|
import { buildToolAliases } from "./skills/index.js";
|
|
23
23
|
import { validateWorkflow } from "./schema.js";
|
|
24
24
|
import { grantedAgentEnv, resolveAgentAccess } from "./agent-env.js";
|
|
25
|
+
import { gitCredentialWarning, scanGitCredentials } from "./git-credentials.js";
|
|
26
|
+
// #473: arms the sweny-side branch push in `github_create_pr` (Node only; skills/ stays browser-safe).
|
|
27
|
+
import "./skills/git-push.js";
|
|
25
28
|
import { fenceUntrusted } from "./untrusted.js";
|
|
26
29
|
import { asClaude } from "./harness/compat.js";
|
|
27
30
|
import { budgetGate, isToolClass, policyGate, resolveHarnessPolicy } from "./harness/policy.js";
|
|
@@ -89,6 +92,17 @@ export async function execute(workflow, input, options) {
|
|
|
89
92
|
// `budget` and the caller's (`--max-tokens`, `--max-cost`).
|
|
90
93
|
const budgetGuard = new BudgetGuard(minLimits(toLimits(workflow.budget), toLimits(options.budget)));
|
|
91
94
|
const harnessPolicy = options.harnessPolicy ?? resolveHarnessPolicy(runEnv);
|
|
95
|
+
// #473: a checkout that persisted a git credential (actions/checkout's
|
|
96
|
+
// default). Each harness masks it for read-only and staged nodes, or reports
|
|
97
|
+
// the node degraded; this says so once per run. Paths and keys, never values.
|
|
98
|
+
try {
|
|
99
|
+
const credentialWarning = gitCredentialWarning(scanGitCredentials(process.cwd(), { env: runEnv }));
|
|
100
|
+
if (credentialWarning)
|
|
101
|
+
logger.warn(credentialWarning);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// A scan failure never stops a run; each node scans again before it starts.
|
|
105
|
+
}
|
|
92
106
|
// Decision model (#357): shadow only. Null (the default) means no HTTP at all.
|
|
93
107
|
const shadow = createShadowDecider(workflow.decider, options.decider, runEnv, (m) => logger.warn(m));
|
|
94
108
|
if (shadow)
|
|
@@ -419,6 +433,7 @@ export async function execute(workflow, input, options) {
|
|
|
419
433
|
const attemptBudget = budgetOn ? nodeBudget.attempt(signal) : undefined;
|
|
420
434
|
try {
|
|
421
435
|
result = await claude.run({
|
|
436
|
+
nodeId: currentId,
|
|
422
437
|
instruction: currentInstruction,
|
|
423
438
|
context,
|
|
424
439
|
tools: trackedTools,
|
|
@@ -910,8 +925,12 @@ function contextDependencies(workflow, nodeId, instruction = "") {
|
|
|
910
925
|
}
|
|
911
926
|
return deps;
|
|
912
927
|
}
|
|
913
|
-
/**
|
|
914
|
-
|
|
928
|
+
/**
|
|
929
|
+
* The bounded prompt context for one node: `input` plus its dependencies' entries.
|
|
930
|
+
* Exported for the property tests only; not part of the package API (index.ts does not re-export it).
|
|
931
|
+
* @internal
|
|
932
|
+
*/
|
|
933
|
+
export function buildBoundedContext(workflow, nodeId, results, input, instruction) {
|
|
915
934
|
const deps = contextDependencies(workflow, nodeId, instruction);
|
|
916
935
|
return {
|
|
917
936
|
input,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted git credentials in the checkout (#473).
|
|
3
|
+
*
|
|
4
|
+
* `actions/checkout` keeps the job token on disk by default
|
|
5
|
+
* (`persist-credentials: true`): v4 and v5 write it into `.git/config` as an
|
|
6
|
+
* `http.<origin>/.extraheader`; v6 and later write it to a separate file under
|
|
7
|
+
* `$RUNNER_TEMP` that `.git/config` pulls in with `includeIf.gitdir:...path`.
|
|
8
|
+
* Either way, any agent that can read the repo can read a token that pushes to
|
|
9
|
+
* it. Env scoping cannot reach a file.
|
|
10
|
+
*
|
|
11
|
+
* {@link scanGitCredentials} finds the files that hold such a credential, so a
|
|
12
|
+
* harness can make them unreadable to the agent of a read-only or staged node
|
|
13
|
+
* (the process wrapper's `denyRead`, Claude Code's `Read(...)` deny rules and
|
|
14
|
+
* SDK sandbox `denyRead`), or report the node `degraded` (refused under strict)
|
|
15
|
+
* where it cannot. The fix that needs no masking at all is
|
|
16
|
+
* `persist-credentials: false`: sweny pushes the PR branch itself
|
|
17
|
+
* (`github_create_pr`), with the skill's own token, in the sweny process.
|
|
18
|
+
*
|
|
19
|
+
* What counts as a credential:
|
|
20
|
+
* - `http.extraheader` / `http.<url>.extraheader` (the checkout's header);
|
|
21
|
+
* - `credential.helper` with an inline secret (`!f() { echo password=...; }; f`);
|
|
22
|
+
* a `store` helper's file (default `~/.git-credentials`, or `--file <path>`);
|
|
23
|
+
* - `url.<base>.insteadOf` / `pushInsteadOf` whose base carries userinfo with
|
|
24
|
+
* a password or a token, and `remote.<name>.url` / `pushurl` that do.
|
|
25
|
+
*
|
|
26
|
+
* Files scanned: the repo config (`<commondir>/config`), the worktree config
|
|
27
|
+
* (`<gitdir>/config.worktree`), the global config (`GIT_CONFIG_GLOBAL`, else
|
|
28
|
+
* `~/.gitconfig` and `$XDG_CONFIG_HOME/git/config`), and every file they
|
|
29
|
+
* include (`include.path`, and `includeIf.*.path` whatever the condition).
|
|
30
|
+
* Values never leave this module: findings carry the file and a redacted key.
|
|
31
|
+
*/
|
|
32
|
+
export { gitCredentialGap } from "./harness/policy.js";
|
|
33
|
+
export type GitCredentialKind = "extraheader" | "credential-helper" | "credential-store" | "url-userinfo";
|
|
34
|
+
export interface GitCredentialFinding {
|
|
35
|
+
/** The file holding the credential (absolute, symlinks resolved). */
|
|
36
|
+
file: string;
|
|
37
|
+
/** The config key, with any embedded credential redacted (`url.<redacted>.insteadof`). */
|
|
38
|
+
key: string;
|
|
39
|
+
kind: GitCredentialKind;
|
|
40
|
+
}
|
|
41
|
+
export interface GitCredentialScan {
|
|
42
|
+
findings: GitCredentialFinding[];
|
|
43
|
+
/** Every file to keep from the agent, deduplicated, in scan order. */
|
|
44
|
+
files: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface ScanGitCredentialsOptions {
|
|
47
|
+
env?: Record<string, string | undefined>;
|
|
48
|
+
/** Test seam. Default: `fs.readFileSync(p, "utf8")`; a throw means "not there". */
|
|
49
|
+
readFile?: (p: string) => string;
|
|
50
|
+
/** Test seam. Default: `fs.existsSync`. */
|
|
51
|
+
exists?: (p: string) => boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Does a URL carry a credential in its userinfo (`https://user:pass@`, `https://<token>@`)? */
|
|
54
|
+
export declare function urlHasCredential(url: string): boolean;
|
|
55
|
+
/** Does a `credential.helper` value hold the secret itself (an inline shell helper)? */
|
|
56
|
+
export declare function helperHasInlineSecret(value: string): boolean;
|
|
57
|
+
/** The file a `store` credential helper writes, or undefined when the helper is not `store`. */
|
|
58
|
+
export declare function storeHelperFiles(value: string, env: Record<string, string | undefined>): string[] | undefined;
|
|
59
|
+
interface ConfigEntry {
|
|
60
|
+
section: string;
|
|
61
|
+
/** Case preserved, as git does. */
|
|
62
|
+
subsection?: string;
|
|
63
|
+
key: string;
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Minimal git-config parser: `[section]`, `[section "sub"]`, the legacy
|
|
68
|
+
* `[section.sub]`, `key = value`, bare `key` (boolean), quoted values with
|
|
69
|
+
* escapes, `#` and `;` comments, and `\` line continuations. Section and key
|
|
70
|
+
* names are lowercased; subsections keep their case. Enough to find keys; not
|
|
71
|
+
* a validator.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseGitConfig(text: string): ConfigEntry[];
|
|
74
|
+
/** The repo's git dir and common dir for `cwd`, walking up to the filesystem root. */
|
|
75
|
+
export declare function findGitDirs(cwd: string, readFile: (p: string) => string, exists: (p: string) => boolean): {
|
|
76
|
+
gitDir: string;
|
|
77
|
+
commonDir: string;
|
|
78
|
+
} | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Find persisted git credentials visible from `cwd`. Pure apart from reading
|
|
81
|
+
* config files; never returns a credential value.
|
|
82
|
+
*/
|
|
83
|
+
export declare function scanGitCredentials(cwd: string, opts?: ScanGitCredentialsOptions): GitCredentialScan;
|
|
84
|
+
/**
|
|
85
|
+
* The files to keep from one node's agent: the scan's files for a read-only
|
|
86
|
+
* node or a staged write node, nothing otherwise. A default write node keeps
|
|
87
|
+
* today's behavior (it may push with the checkout's credential); the
|
|
88
|
+
* recommended `persist-credentials: false` leaves nothing to find.
|
|
89
|
+
*/
|
|
90
|
+
export declare function gitCredentialMask(cwd: string, node: {
|
|
91
|
+
readOnly: boolean;
|
|
92
|
+
staged: boolean;
|
|
93
|
+
}, opts?: ScanGitCredentialsOptions): string[];
|
|
94
|
+
/** The run-start warning for a checkout that persists a credential. Paths and kinds only. */
|
|
95
|
+
export declare function gitCredentialWarning(scan: GitCredentialScan): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* The `NodePolicy.gitCredentials` fragment for one node run (#473): the
|
|
98
|
+
* credential files to keep from a read-only or staged node's agent, or
|
|
99
|
+
* nothing. Adapters spread it into the policy they gate and wrap with.
|
|
100
|
+
*/
|
|
101
|
+
export declare function gitCredentialPolicy(cwd: string, node: {
|
|
102
|
+
readOnly: boolean;
|
|
103
|
+
noPush?: boolean;
|
|
104
|
+
}, opts?: ScanGitCredentialsOptions): {
|
|
105
|
+
gitCredentials?: string[];
|
|
106
|
+
};
|