@snowyroad/braid 0.98.0 → 0.98.1
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/cli.js +82 -13
- package/package.json +1 -1
- package/service-shrinkwrap.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2613,6 +2613,17 @@ function oneLine(s, max = 300) {
|
|
|
2613
2613
|
function errText(e) {
|
|
2614
2614
|
return oneLine(e instanceof Error ? e.message : String(e));
|
|
2615
2615
|
}
|
|
2616
|
+
var GH_OWNER_RE = /^[A-Za-z0-9-]{1,100}$/;
|
|
2617
|
+
var GH_REPO_RE = /^(?!\.\.?$)[A-Za-z0-9._-]{1,100}$/;
|
|
2618
|
+
function parseGitHubOrigin(url) {
|
|
2619
|
+
const s = url.trim();
|
|
2620
|
+
const m = /^https:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(s) ?? /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::22)?\/(.+)$/i.exec(s) ?? /^(?:[^@/\s:]+@)?github\.com:(?!\/)(.+)$/i.exec(s);
|
|
2621
|
+
if (!m) return null;
|
|
2622
|
+
const parts = m[1].replace(/\/$/, "").replace(/\.git$/i, "").split("/");
|
|
2623
|
+
if (parts.length !== 2) return null;
|
|
2624
|
+
const [owner, name] = parts;
|
|
2625
|
+
return GH_OWNER_RE.test(owner) && GH_REPO_RE.test(name) ? { owner, name } : null;
|
|
2626
|
+
}
|
|
2616
2627
|
function shellQuote(p) {
|
|
2617
2628
|
return `'${p.replace(/'/g, `'\\''`)}'`;
|
|
2618
2629
|
}
|
|
@@ -2625,6 +2636,9 @@ function safeSegment(s) {
|
|
|
2625
2636
|
function logSafe(s) {
|
|
2626
2637
|
return s.replace(/[^A-Za-z0-9._\/-]/g, "?").slice(0, 200) || "-";
|
|
2627
2638
|
}
|
|
2639
|
+
function repoKey(g) {
|
|
2640
|
+
return `${g.repoOwner}/${g.repoName}`.toLowerCase();
|
|
2641
|
+
}
|
|
2628
2642
|
function isAutonomous(g) {
|
|
2629
2643
|
return g.mode === "autonomous";
|
|
2630
2644
|
}
|
|
@@ -2677,7 +2691,7 @@ var GitGateway = class {
|
|
|
2677
2691
|
async statusInner(channelId, args) {
|
|
2678
2692
|
const gate = this.gateChecks(channelId);
|
|
2679
2693
|
if (gate) return gate;
|
|
2680
|
-
const grant = await this.
|
|
2694
|
+
const grant = await this.resolveGrant(channelId, args.path, true);
|
|
2681
2695
|
if (!grant.ok) return grant;
|
|
2682
2696
|
const g = grant.grant;
|
|
2683
2697
|
const wt = this.worktree(channelId, args.path);
|
|
@@ -2717,12 +2731,12 @@ var GitGateway = class {
|
|
|
2717
2731
|
`Worktree ${wt.path}: branch ${branch ?? "unknown"}, HEAD ${head ?? "none"}.`,
|
|
2718
2732
|
tracking
|
|
2719
2733
|
];
|
|
2720
|
-
return { ok: true, text: lines.join("\n"), data: { repo, mode, branch, head, ahead, behind } };
|
|
2734
|
+
return { ok: true, text: lines.join("\n"), data: { repo, mode, source: g.source, branch, head, ahead, behind } };
|
|
2721
2735
|
}
|
|
2722
2736
|
async fetchInner(channelId, args) {
|
|
2723
2737
|
const gate = this.gateChecks(channelId);
|
|
2724
2738
|
if (gate) return gate;
|
|
2725
|
-
const grant = await this.
|
|
2739
|
+
const grant = await this.resolveGrant(channelId, args.path, true);
|
|
2726
2740
|
if (!grant.ok) return grant;
|
|
2727
2741
|
const g = grant.grant;
|
|
2728
2742
|
const mirror = this.mirrorPath(g);
|
|
@@ -2767,7 +2781,7 @@ ${shown}${more}`,
|
|
|
2767
2781
|
async pushInner(channelId, branch, args, out) {
|
|
2768
2782
|
const gate = this.gateChecks(channelId);
|
|
2769
2783
|
if (gate) return gate;
|
|
2770
|
-
const grant = await this.
|
|
2784
|
+
const grant = await this.resolveGrant(channelId, args.path, true);
|
|
2771
2785
|
if (!grant.ok) return grant;
|
|
2772
2786
|
const listed = grant.grant;
|
|
2773
2787
|
const repo = `${listed.repoOwner}/${listed.repoName}`;
|
|
@@ -2840,7 +2854,7 @@ Compare: https://github.com/${repo}/compare/${encodeURIComponent(branch)}`,
|
|
|
2840
2854
|
async openPrInner(channelId, args) {
|
|
2841
2855
|
const gate = this.gateChecks(channelId);
|
|
2842
2856
|
if (gate) return gate;
|
|
2843
|
-
const grant = await this.
|
|
2857
|
+
const grant = await this.resolveGrant(channelId, args.path, false);
|
|
2844
2858
|
if (!grant.ok) return grant;
|
|
2845
2859
|
const branch = String(args.branch ?? "");
|
|
2846
2860
|
const badHead = await this.checkBranch(branch);
|
|
@@ -2876,7 +2890,7 @@ Compare: https://github.com/${repo}/compare/${encodeURIComponent(branch)}`,
|
|
|
2876
2890
|
async mergePrInner(channelId, args) {
|
|
2877
2891
|
const gate = this.gateChecks(channelId);
|
|
2878
2892
|
if (gate) return gate;
|
|
2879
|
-
const grant = await this.
|
|
2893
|
+
const grant = await this.resolveGrant(channelId, void 0, false);
|
|
2880
2894
|
if (!grant.ok) return grant;
|
|
2881
2895
|
const num = Number(args.number);
|
|
2882
2896
|
if (!Number.isInteger(num) || num <= 0) return fail(400, "invalid_number", "Pass the pull request number as a positive integer.");
|
|
@@ -2963,17 +2977,70 @@ Compare: https://github.com/${repo}/compare/${encodeURIComponent(branch)}`,
|
|
|
2963
2977
|
if (this.disabledDepth.has(channelId)) return fail(403, "git_disabled_for_turn", "Git tools are disabled for this turn. Describe the change in your reply instead, and a later turn can push it.");
|
|
2964
2978
|
return null;
|
|
2965
2979
|
}
|
|
2966
|
-
/**
|
|
2967
|
-
|
|
2980
|
+
/** D11 + R15: exactly one grant (server or PAT) is used as is, whatever the worktree's
|
|
2981
|
+
* origin (a fork origin keeps working, as in 0.98.0). When every grant is for the same
|
|
2982
|
+
* repo (case-insensitive owner/name, e.g. a server grant plus a local PAT), the repo is
|
|
2983
|
+
* unambiguous: no origin lookup, and grantForRepo picks the server grant over the PAT.
|
|
2984
|
+
* With grants for two or more distinct repos, "the repo" is the
|
|
2985
|
+
* worktree's GitHub origin and its grant is the server grant when one exists, else the
|
|
2986
|
+
* local PAT (resolver.grantForRepo); an origin matching no grant is no_grant_for_repo and
|
|
2987
|
+
* no usable origin is no_grant_for_worktree. The grant is still the only source of the
|
|
2988
|
+
* push/fetch target (D8.1): the origin only selects among grants the owner already made,
|
|
2989
|
+
* never names a destination. Ops that do not need a worktree (open/merge PR without a
|
|
2990
|
+
* path) treat an unusable cwd as "no origin". */
|
|
2991
|
+
async resolveGrant(channelId, path2, needWorktree) {
|
|
2968
2992
|
let grants;
|
|
2969
2993
|
try {
|
|
2970
2994
|
grants = await this.deps.resolver.listGrants();
|
|
2971
2995
|
} catch (e) {
|
|
2972
2996
|
return fail(502, "grants_unavailable", `Could not load this agent's git grants (${errText(e)}). Try again in a minute.`);
|
|
2973
2997
|
}
|
|
2974
|
-
|
|
2975
|
-
if (grants.length
|
|
2976
|
-
return { ok: true, grant: grants[0] };
|
|
2998
|
+
const where = `Agents > ${this.deps.agentName} > Git access`;
|
|
2999
|
+
if (grants.length === 0) return fail(404, "no_grant", `No git grant for this agent. Your owner can add one under ${where}.`);
|
|
3000
|
+
if (grants.length === 1) return { ok: true, grant: grants[0] };
|
|
3001
|
+
if (new Set(grants.map(repoKey)).size === 1) {
|
|
3002
|
+
const only = grants[0];
|
|
3003
|
+
let g2;
|
|
3004
|
+
try {
|
|
3005
|
+
g2 = await this.deps.resolver.grantForRepo(only.repoOwner, only.repoName);
|
|
3006
|
+
} catch (e) {
|
|
3007
|
+
return fail(502, "grants_unavailable", `Could not load this agent's git grants (${errText(e)}). Try again in a minute.`);
|
|
3008
|
+
}
|
|
3009
|
+
return { ok: true, grant: g2 ?? only };
|
|
3010
|
+
}
|
|
3011
|
+
const wt = this.worktree(channelId, path2);
|
|
3012
|
+
if (!wt.ok && (needWorktree || path2 !== void 0)) return wt;
|
|
3013
|
+
const origin = wt.ok ? await this.worktreeOrigin(wt) : null;
|
|
3014
|
+
if (origin && !origin.ok) return origin;
|
|
3015
|
+
const repo = origin?.repo ?? null;
|
|
3016
|
+
if (!repo) {
|
|
3017
|
+
const example = `${grants[0].repoOwner}/${grants[0].repoName}`;
|
|
3018
|
+
return fail(404, "no_grant_for_worktree", `This worktree has no GitHub origin remote and the agent has ${grants.length} git grants (${this.grantList(grants)}). Add the granted repository as origin (git remote add origin https://github.com/${example}.git, using the repository you are working on) so the gateway knows which grant applies.`);
|
|
3019
|
+
}
|
|
3020
|
+
let g;
|
|
3021
|
+
try {
|
|
3022
|
+
g = await this.deps.resolver.grantForRepo(repo.owner, repo.name);
|
|
3023
|
+
} catch (e) {
|
|
3024
|
+
return fail(502, "grants_unavailable", `Could not load this agent's git grants (${errText(e)}). Try again in a minute.`);
|
|
3025
|
+
}
|
|
3026
|
+
if (!g) return fail(404, "no_grant_for_repo", `This worktree's origin is ${repo.owner}/${repo.name}, and the agent has no git grant for that repository (its grants: ${this.grantList(grants)}). Your owner can add ${repo.owner}/${repo.name} under ${where}.`);
|
|
3027
|
+
return { ok: true, grant: g };
|
|
3028
|
+
}
|
|
3029
|
+
grantList(grants) {
|
|
3030
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3031
|
+
for (const x of grants) if (!byKey.has(repoKey(x))) byKey.set(repoKey(x), `${x.repoOwner}/${x.repoName}`);
|
|
3032
|
+
const names = [...byKey.values()];
|
|
3033
|
+
return names.slice(0, 10).join(", ") + (names.length > 10 ? `, and ${names.length - 10} more` : "");
|
|
3034
|
+
}
|
|
3035
|
+
/** R15: remote.origin.url of the validated worktree, read with the hardened runner while
|
|
3036
|
+
* the worktree identity is held; null repo = no usable (GitHub) origin. */
|
|
3037
|
+
async worktreeOrigin(wt) {
|
|
3038
|
+
const changed = fail(400, "invalid_path", "The worktree changed while its origin remote was being read (its .git was replaced or gained alternates or a config include). Make sure nothing else is modifying the repository, then try again.");
|
|
3039
|
+
const id = this.identity(wt);
|
|
3040
|
+
if (!id) return changed;
|
|
3041
|
+
const r = await this.git(["config", "--get", "remote.origin.url"], { gitDir: wt.gitDir, cwd: wt.path });
|
|
3042
|
+
if (!this.sameIdentity(wt, id)) return changed;
|
|
3043
|
+
return { ok: true, repo: r.code === 0 ? parseGitHubOrigin(r.stdout) : null };
|
|
2977
3044
|
}
|
|
2978
3045
|
/** Credential plus the effective policy. When the listed grant is LOOSER than the
|
|
2979
3046
|
* policy the cached mint carries (the owner loosened the grant after the mint), the
|
|
@@ -3323,11 +3390,13 @@ var GrantResolver = class {
|
|
|
3323
3390
|
}
|
|
3324
3391
|
return out;
|
|
3325
3392
|
}
|
|
3393
|
+
/** D11: a server (app) grant for the repo wins; otherwise the local PAT grant.
|
|
3394
|
+
* GitHub owner and repo names are case-insensitive. */
|
|
3326
3395
|
async grantForRepo(owner, name) {
|
|
3327
3396
|
const o = owner.toLowerCase();
|
|
3328
3397
|
const n = name.toLowerCase();
|
|
3329
|
-
const
|
|
3330
|
-
return
|
|
3398
|
+
const matches = (await this.listGrants()).filter((g) => g.repoOwner.toLowerCase() === o && g.repoName.toLowerCase() === n);
|
|
3399
|
+
return matches.find((g) => g.source === "app") ?? matches.find((g) => g.source === "pat") ?? null;
|
|
3331
3400
|
}
|
|
3332
3401
|
async credentialFor(grant) {
|
|
3333
3402
|
if (grant.source === "pat") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/braid",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.1",
|
|
4
4
|
"description": "Braid: connect your own coding agent (Claude Code, Codex, Gemini, Grok) to a Braid channel and collaborate with other agents and humans.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "SnowyRoad",
|
package/service-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/braid",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@snowyroad/braid",
|
|
9
|
-
"version": "0.98.
|
|
9
|
+
"version": "0.98.1",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "0.25.1",
|