@joe-broadhead/opencode-gws 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +24 -6
- package/assets/icon.svg +1 -0
- package/dist/index.js +975 -352
- package/dist/tui.js +10 -9
- package/package.json +15 -7
- package/skills/opencode-gws/SKILL.md +3 -1
- package/skills/opencode-gws-recipes/SKILL.md +3 -1
- package/views/jelly-view.d.ts +0 -73
package/dist/index.js
CHANGED
|
@@ -5320,8 +5320,8 @@ function rewriteKeyNames(ctx) {
|
|
|
5320
5320
|
bySchema.set(entry.schema, entry);
|
|
5321
5321
|
}
|
|
5322
5322
|
const rewrites = /* @__PURE__ */ new Map();
|
|
5323
|
-
for (const
|
|
5324
|
-
const seen = ctx.seen.get(
|
|
5323
|
+
for (const record3 of pendingRecords.get(ctx) ?? []) {
|
|
5324
|
+
const seen = ctx.seen.get(record3);
|
|
5325
5325
|
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
5326
5326
|
if (!names || names === true || rewrites.has(names))
|
|
5327
5327
|
continue;
|
|
@@ -6575,9 +6575,17 @@ var ErrorCode = {
|
|
|
6575
6575
|
meaning: "Plugin policy refused the call: the category, the service, recipients, public sharing, the admin gate, or a path outside the allowed roots. A deny is final.",
|
|
6576
6576
|
next: "Do not work around it. If the message says to use gws_plan, plan the change; otherwise tell the user, who can change the plugin policy."
|
|
6577
6577
|
},
|
|
6578
|
+
RECIPIENTS_UNKNOWN: {
|
|
6579
|
+
meaning: "A policy rule limits recipients, and the plugin could not tell who this send or share reaches: a raw message without readable To, Cc or Bcc headers, a draft, message or event it could not read, a draft that names no recipient, or a method whose recipients it does not know. Nothing ran.",
|
|
6580
|
+
next: "Name the recipients in the request (to, cc and bcc, or the To, Cc and Bcc headers of raw), or tell the user the policy rule refuses this method."
|
|
6581
|
+
},
|
|
6582
|
+
ACCOUNT_DOMAIN_UNKNOWN: {
|
|
6583
|
+
meaning: "A policy rule sets allowExternalRecipients to false, and the account's own email domain is unknown: accounts.<alias>.domains is unset and the signed-in address could not be read. Nothing ran.",
|
|
6584
|
+
next: "Ask the user to set accounts.<alias>.domains to the account's email domains, or to enable Gmail, Drive or Calendar for the account so the plugin can read its address."
|
|
6585
|
+
},
|
|
6578
6586
|
// ── Plans ─────────────────────────────────────────────────────────────
|
|
6579
6587
|
PLAN_NOT_FOUND: {
|
|
6580
|
-
meaning: "No plan has this planId, or it was removed.",
|
|
6588
|
+
meaning: "No plan has this planId: it never existed, or its time limit passed and it was removed.",
|
|
6581
6589
|
next: "Create a plan with gws_plan, then commit its planId."
|
|
6582
6590
|
},
|
|
6583
6591
|
PLAN_ALREADY_USED: {
|
|
@@ -6599,15 +6607,15 @@ var ErrorCode = {
|
|
|
6599
6607
|
// ── Scripts (the tools RPC) ───────────────────────────────────────────
|
|
6600
6608
|
SESSION_UNKNOWN: {
|
|
6601
6609
|
meaning: "A script named a session that does not exist. A script runs as a session, under that session's agent and its permission rules.",
|
|
6602
|
-
next:
|
|
6610
|
+
next: 'Create a session that names an agent, such as POST /api/session with {"agent":"build"}, and pass its id.'
|
|
6603
6611
|
},
|
|
6604
6612
|
SESSION_AGENT_REQUIRED: {
|
|
6605
6613
|
meaning: "A script named a session that names no agent. A script runs as its session's agent, so its permissions and plans never depend on the default agent.",
|
|
6606
|
-
next:
|
|
6614
|
+
next: 'Create a session that names an agent, such as POST /api/session with {"agent":"build"}, and pass its id.'
|
|
6607
6615
|
},
|
|
6608
6616
|
PERMISSION_DENIED: {
|
|
6609
6617
|
meaning: "The permission rules of the session's agent deny the call, or ask for an approval that a script cannot give, so the call did not run.",
|
|
6610
|
-
next: "
|
|
6618
|
+
next: "Add an allow rule for the tool to the session's agent or to the session, or use a session whose agent allows it."
|
|
6611
6619
|
},
|
|
6612
6620
|
// ── Sign-in ───────────────────────────────────────────────────────────
|
|
6613
6621
|
AUTH_REQUIRED: {
|
|
@@ -6619,7 +6627,7 @@ var ErrorCode = {
|
|
|
6619
6627
|
next: "Ask the user to sign in again with the access the message names, or to add the scope to the account's scopes, then retry. For a method only an app's credentials can run, use another method."
|
|
6620
6628
|
},
|
|
6621
6629
|
ADMIN_PRIVILEGE_REQUIRED: {
|
|
6622
|
-
meaning: "The sign-in is valid, but
|
|
6630
|
+
meaning: "The sign-in is valid, but an Admin SDK service (admin-reports) refused the call because the account lacks a Workspace admin privilege the API needs, such as Reports. Signing in again as the same account does not help, and the sign-in is not marked as needing renewal. Another service's refusal is never this code.",
|
|
6623
6631
|
next: "Ask the user to sign in with a Workspace admin account that has the needed privilege (Reports for admin-reports), or to remove the service from enabledServices."
|
|
6624
6632
|
},
|
|
6625
6633
|
AUTH_CLIENT_REQUIRED: {
|
|
@@ -6706,8 +6714,8 @@ var ErrorCode = {
|
|
|
6706
6714
|
},
|
|
6707
6715
|
// ── Setup: these stop the plugin from loading ────────────────────────
|
|
6708
6716
|
HOST_UNTESTED: {
|
|
6709
|
-
meaning: "The OpenCode version is outside the
|
|
6710
|
-
next: "Use
|
|
6717
|
+
meaning: "The OpenCode version is outside the supported range in package.json opencode.host (>=2.2.3 <2.3.0).",
|
|
6718
|
+
next: "Use OpenCode 2.2.3, 2.2.4 or 2.2.5 (tested) or another release in the range, or set allowUntestedHost to true to run anyway; status then shows the host as untested."
|
|
6711
6719
|
},
|
|
6712
6720
|
OPTIONS_INVALID: {
|
|
6713
6721
|
meaning: "The plugin options are not valid: an unknown key, a wrong type, an unknown service, or access or scopes that name something the account does not enable.",
|
|
@@ -6772,8 +6780,8 @@ var GwsError = class _GwsError extends Error {
|
|
|
6772
6780
|
return stated ? new _GwsError(code2, stated[1], { next: stated[2] }) : new _GwsError(code2, match[2]);
|
|
6773
6781
|
}
|
|
6774
6782
|
};
|
|
6775
|
-
function sentence(
|
|
6776
|
-
const trimmed =
|
|
6783
|
+
function sentence(text4) {
|
|
6784
|
+
const trimmed = text4.trim();
|
|
6777
6785
|
const capital = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
6778
6786
|
return /[.!?]$/.test(capital) ? capital : `${capital}.`;
|
|
6779
6787
|
}
|
|
@@ -6973,22 +6981,29 @@ var replyBase = {
|
|
|
6973
6981
|
messageId: string2().min(1),
|
|
6974
6982
|
body: string2().min(1).max(1e5),
|
|
6975
6983
|
from: string2().email().optional(),
|
|
6984
|
+
to: union([string2(), array(string2())]).optional().describe("Extra To recipients, besides the ones the reply goes to"),
|
|
6976
6985
|
cc: union([string2(), array(string2())]).optional(),
|
|
6977
6986
|
bcc: union([string2(), array(string2())]).optional(),
|
|
6978
6987
|
html: boolean2().optional(),
|
|
6979
6988
|
draft: boolean2().optional(),
|
|
6980
6989
|
attachments: array(absolutePath).max(20).optional()
|
|
6981
6990
|
};
|
|
6982
|
-
|
|
6991
|
+
var replyAllBase = {
|
|
6992
|
+
...replyBase,
|
|
6993
|
+
remove: union([string2(), array(string2())]).optional().describe("Addresses to leave out of the reply-all, the sender's included")
|
|
6994
|
+
};
|
|
6995
|
+
function replyAdapter(id, name, all) {
|
|
6983
6996
|
return {
|
|
6984
6997
|
id,
|
|
6985
6998
|
service: "gmail",
|
|
6986
6999
|
category: "send",
|
|
6987
|
-
inputSchema: strictObject(replyBase),
|
|
7000
|
+
inputSchema: strictObject(all ? replyAllBase : replyBase),
|
|
6988
7001
|
build(input, request) {
|
|
6989
7002
|
const parsed = this.inputSchema.parse(input);
|
|
6990
7003
|
const argv = ["gmail", name, "--message-id", parsed.messageId, "--body", parsed.body];
|
|
6991
7004
|
if (parsed.from) argv.push("--from", parsed.from);
|
|
7005
|
+
if (parsed.to) argv.push("--to", emails(parsed.to).join(","));
|
|
7006
|
+
if (parsed.remove) argv.push("--remove", emails(parsed.remove).join(","));
|
|
6992
7007
|
if (parsed.cc) argv.push("--cc", emails(parsed.cc).join(","));
|
|
6993
7008
|
if (parsed.bcc) argv.push("--bcc", emails(parsed.bcc).join(","));
|
|
6994
7009
|
if (parsed.html) argv.push("--html");
|
|
@@ -7001,9 +7016,9 @@ function replyAdapter(id, name) {
|
|
|
7001
7016
|
files,
|
|
7002
7017
|
preview: {
|
|
7003
7018
|
method: `gmail ${name}`,
|
|
7004
|
-
params: { messageId: parsed.messageId, cc: emails(parsed.cc), bcc: emails(parsed.bcc) },
|
|
7019
|
+
params: { messageId: parsed.messageId, to: emails(parsed.to), cc: emails(parsed.cc), bcc: emails(parsed.bcc), ...all ? { remove: emails(parsed.remove) } : {}, draft: parsed.draft ?? false },
|
|
7005
7020
|
bodySummary: bytes(parsed.body),
|
|
7006
|
-
recipients: [...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
7021
|
+
recipients: [...emails(parsed.to), ...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
7007
7022
|
uploads: parsed.attachments,
|
|
7008
7023
|
account: request.account,
|
|
7009
7024
|
category: "send"
|
|
@@ -7012,30 +7027,45 @@ function replyAdapter(id, name) {
|
|
|
7012
7027
|
}
|
|
7013
7028
|
};
|
|
7014
7029
|
}
|
|
7030
|
+
var forwardSchema = strictObject({
|
|
7031
|
+
messageId: string2().min(1),
|
|
7032
|
+
to: union([string2(), array(string2())]),
|
|
7033
|
+
cc: union([string2(), array(string2())]).optional(),
|
|
7034
|
+
bcc: union([string2(), array(string2())]).optional(),
|
|
7035
|
+
from: string2().email().optional(),
|
|
7036
|
+
body: string2().max(1e5).optional().describe("A note above the forwarded message"),
|
|
7037
|
+
html: boolean2().optional(),
|
|
7038
|
+
draft: boolean2().optional(),
|
|
7039
|
+
attachments: array(absolutePath).max(20).optional(),
|
|
7040
|
+
noOriginalAttachments: boolean2().optional().describe("Leave out the original message's attachments")
|
|
7041
|
+
});
|
|
7015
7042
|
var gmailForward = {
|
|
7016
7043
|
id: "gmail.forward",
|
|
7017
7044
|
service: "gmail",
|
|
7018
7045
|
category: "send",
|
|
7019
|
-
inputSchema:
|
|
7020
|
-
messageId: string2().min(1),
|
|
7021
|
-
to: union([string2(), array(string2())]),
|
|
7022
|
-
body: string2().max(1e5).optional(),
|
|
7023
|
-
html: boolean2().optional()
|
|
7024
|
-
}),
|
|
7046
|
+
inputSchema: forwardSchema,
|
|
7025
7047
|
build(input, request) {
|
|
7026
7048
|
const parsed = this.inputSchema.parse(input);
|
|
7027
7049
|
const argv = ["gmail", "+forward", "--message-id", parsed.messageId, "--to", emails(parsed.to).join(",")];
|
|
7050
|
+
if (parsed.cc) argv.push("--cc", emails(parsed.cc).join(","));
|
|
7051
|
+
if (parsed.bcc) argv.push("--bcc", emails(parsed.bcc).join(","));
|
|
7052
|
+
if (parsed.from) argv.push("--from", parsed.from);
|
|
7028
7053
|
if (parsed.body) argv.push("--body", parsed.body);
|
|
7029
7054
|
if (parsed.html) argv.push("--html");
|
|
7055
|
+
if (parsed.draft) argv.push("--draft");
|
|
7056
|
+
if (parsed.noOriginalAttachments) argv.push("--no-original-attachments");
|
|
7057
|
+
const files = attach(argv, parsed.attachments);
|
|
7030
7058
|
return {
|
|
7031
7059
|
id: this.id,
|
|
7032
7060
|
category: "send",
|
|
7033
7061
|
argv,
|
|
7062
|
+
files,
|
|
7034
7063
|
preview: {
|
|
7035
7064
|
method: "gmail +forward",
|
|
7036
|
-
params: { messageId: parsed.messageId, to: emails(parsed.to) },
|
|
7065
|
+
params: { messageId: parsed.messageId, to: emails(parsed.to), cc: emails(parsed.cc), bcc: emails(parsed.bcc), draft: parsed.draft ?? false },
|
|
7037
7066
|
bodySummary: parsed.body ? bytes(parsed.body) : void 0,
|
|
7038
|
-
recipients: emails(parsed.to),
|
|
7067
|
+
recipients: [...emails(parsed.to), ...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
7068
|
+
uploads: parsed.attachments,
|
|
7039
7069
|
account: request.account,
|
|
7040
7070
|
category: "send"
|
|
7041
7071
|
}
|
|
@@ -7049,7 +7079,8 @@ var gmailTriage = {
|
|
|
7049
7079
|
inputSchema: strictObject({
|
|
7050
7080
|
max: number2().int().positive().max(200).optional(),
|
|
7051
7081
|
query: string2().max(2e3).optional(),
|
|
7052
|
-
labels: boolean2().optional()
|
|
7082
|
+
labels: boolean2().optional(),
|
|
7083
|
+
format: outputFormat.optional()
|
|
7053
7084
|
}),
|
|
7054
7085
|
build(input, request) {
|
|
7055
7086
|
const parsed = this.inputSchema.parse(input);
|
|
@@ -7057,6 +7088,7 @@ var gmailTriage = {
|
|
|
7057
7088
|
if (parsed.max) argv.push("--max", String(parsed.max));
|
|
7058
7089
|
if (parsed.query) argv.push("--query", parsed.query);
|
|
7059
7090
|
if (parsed.labels) argv.push("--labels");
|
|
7091
|
+
if (parsed.format) argv.push("--format", parsed.format);
|
|
7060
7092
|
return {
|
|
7061
7093
|
id: this.id,
|
|
7062
7094
|
category: "read",
|
|
@@ -7217,8 +7249,8 @@ var weeklyDigest = {
|
|
|
7217
7249
|
var ADAPTERS = /* @__PURE__ */ new Map([
|
|
7218
7250
|
[gmailSend.id, gmailSend],
|
|
7219
7251
|
[gmailRead.id, gmailRead],
|
|
7220
|
-
["gmail.reply", replyAdapter("gmail.reply", "+reply")],
|
|
7221
|
-
["gmail.reply-all", replyAdapter("gmail.reply-all", "+reply-all")],
|
|
7252
|
+
["gmail.reply", replyAdapter("gmail.reply", "+reply", false)],
|
|
7253
|
+
["gmail.reply-all", replyAdapter("gmail.reply-all", "+reply-all", true)],
|
|
7222
7254
|
[gmailForward.id, gmailForward],
|
|
7223
7255
|
[gmailTriage.id, gmailTriage],
|
|
7224
7256
|
[calendarAgenda.id, calendarAgenda],
|
|
@@ -7318,9 +7350,9 @@ function commitTool(category) {
|
|
|
7318
7350
|
function words(query) {
|
|
7319
7351
|
return (query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
7320
7352
|
}
|
|
7321
|
-
function startsWord(
|
|
7322
|
-
for (let index2 =
|
|
7323
|
-
if (index2 === 0 || !/[a-z0-9]/.test(
|
|
7353
|
+
function startsWord(text4, token) {
|
|
7354
|
+
for (let index2 = text4.indexOf(token); index2 !== -1; index2 = text4.indexOf(token, index2 + 1)) {
|
|
7355
|
+
if (index2 === 0 || !/[a-z0-9]/.test(text4[index2 - 1])) return true;
|
|
7324
7356
|
}
|
|
7325
7357
|
return false;
|
|
7326
7358
|
}
|
|
@@ -7634,12 +7666,14 @@ function defaultAccounts() {
|
|
|
7634
7666
|
return { default: { mode: "interactive", enabledServices: services, access: "write" } };
|
|
7635
7667
|
}
|
|
7636
7668
|
var enabledServices = array(string2().min(1)).min(1);
|
|
7669
|
+
var domains = array(string2().regex(/^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+$/u, "must be a domain such as example.com")).min(1);
|
|
7637
7670
|
var accountProfile = preprocess(
|
|
7638
7671
|
(value) => value !== null && typeof value === "object" && !Array.isArray(value) && !("mode" in value) ? { ...value, mode: "interactive" } : value,
|
|
7639
7672
|
discriminatedUnion("mode", [
|
|
7640
7673
|
strictObject({
|
|
7641
7674
|
mode: literal("interactive"),
|
|
7642
7675
|
enabledServices,
|
|
7676
|
+
domains: domains.optional(),
|
|
7643
7677
|
access: union([accessLevel, record(string2().min(1), accessLevel)]).optional(),
|
|
7644
7678
|
scopes: array(string2().min(1)).optional(),
|
|
7645
7679
|
oauthClient: OAuthClientSchema.optional()
|
|
@@ -7647,11 +7681,13 @@ var accountProfile = preprocess(
|
|
|
7647
7681
|
strictObject({
|
|
7648
7682
|
mode: literal("credentials_file"),
|
|
7649
7683
|
enabledServices,
|
|
7684
|
+
domains: domains.optional(),
|
|
7650
7685
|
credentialsFileEnv: envVarName
|
|
7651
7686
|
}),
|
|
7652
7687
|
strictObject({
|
|
7653
7688
|
mode: literal("access_token"),
|
|
7654
7689
|
enabledServices,
|
|
7690
|
+
domains: domains.optional(),
|
|
7655
7691
|
tokenEnv: envVarName
|
|
7656
7692
|
})
|
|
7657
7693
|
])
|
|
@@ -7775,6 +7811,7 @@ function loadConfig(raw) {
|
|
|
7775
7811
|
accounts[alias] = {
|
|
7776
7812
|
mode: profile.mode,
|
|
7777
7813
|
enabledServices: enabled,
|
|
7814
|
+
...profile.domains ? { domains: [...new Set(profile.domains.map((domain) => domain.toLowerCase()))] } : {},
|
|
7778
7815
|
access,
|
|
7779
7816
|
...accessSpecified ? { accessSpecified } : {},
|
|
7780
7817
|
scopes: [...new Set(scopes)],
|
|
@@ -7841,10 +7878,9 @@ function loadConfig(raw) {
|
|
|
7841
7878
|
};
|
|
7842
7879
|
}
|
|
7843
7880
|
|
|
7844
|
-
// src/domain/
|
|
7845
|
-
var PLUGIN_VERSION = "0.0.2";
|
|
7846
|
-
var PLUGIN_ID = "google-workspace.gws";
|
|
7881
|
+
// src/domain/host.ts
|
|
7847
7882
|
var HOST_RANGE = ">=2.2.3 <2.3.0";
|
|
7883
|
+
var TESTED_HOSTS = ["2.2.3", "2.2.4", "2.2.5"];
|
|
7848
7884
|
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7849
7885
|
var RANGE = /^>=(\d+)\.(\d+)\.(\d+) <(\d+)\.(\d+)\.(\d+)$/;
|
|
7850
7886
|
function compare(a, b) {
|
|
@@ -7863,18 +7899,20 @@ function satisfies(version2, range) {
|
|
|
7863
7899
|
return compare(triple, floor) >= 0 && compare(triple, ceiling) < 0;
|
|
7864
7900
|
}
|
|
7865
7901
|
function checkHost(version2, range, allowUntested) {
|
|
7866
|
-
const
|
|
7867
|
-
if (satisfies(
|
|
7868
|
-
const problem = `tested with OpenCode ${range}, found ${text3}`;
|
|
7869
|
-
if (!allowUntested) {
|
|
7902
|
+
const text4 = typeof version2 === "string" && version2.trim() ? version2.trim() : void 0;
|
|
7903
|
+
if (!(text4 && satisfies(text4, range)) && !allowUntested) {
|
|
7870
7904
|
const [major, minor, patch] = bounds(range).floor;
|
|
7871
7905
|
throw new Error(
|
|
7872
|
-
`HOST_UNTESTED:
|
|
7906
|
+
`HOST_UNTESTED: Supports OpenCode ${range}, found ${text4 ?? "unknown"}. Next step: Use OpenCode ${major}.${minor}.${patch} or a later ${major}.${minor}.x release, or set "allowUntestedHost": true.`
|
|
7873
7907
|
);
|
|
7874
7908
|
}
|
|
7875
|
-
return { version:
|
|
7909
|
+
return { version: text4 ?? null, range, tested: text4 !== void 0 && TESTED_HOSTS.includes(text4.replace(/^v/, "")) };
|
|
7876
7910
|
}
|
|
7877
7911
|
|
|
7912
|
+
// src/domain/version.ts
|
|
7913
|
+
var PLUGIN_VERSION = "0.0.4";
|
|
7914
|
+
var PLUGIN_ID = "google-workspace.gws";
|
|
7915
|
+
|
|
7878
7916
|
// src/host/agents.ts
|
|
7879
7917
|
var ASK_ACTIONS = ["gws.write", "gws.export", "gws.send", "gws.share", "gws.delete", "gws.admin"];
|
|
7880
7918
|
var ASK_RULES = ASK_ACTIONS.map((action) => ({ action, resource: "*", effect: "ask" }));
|
|
@@ -7950,6 +7988,7 @@ var object2 = (properties, required2) => ({ type: "object", properties, required
|
|
|
7950
7988
|
var MODES = ["interactive", "credentials_file", "access_token"];
|
|
7951
7989
|
var ACCESS = ["read", "write"];
|
|
7952
7990
|
var CLIENT_SOURCES = ["account", "plugin", "pasted"];
|
|
7991
|
+
var STATES = ["connected", "sign_in_required", "not_configured", "host_untested"];
|
|
7953
7992
|
var account = object2(
|
|
7954
7993
|
{
|
|
7955
7994
|
alias: text,
|
|
@@ -7969,9 +8008,9 @@ var account = object2(
|
|
|
7969
8008
|
);
|
|
7970
8009
|
var status = object2(
|
|
7971
8010
|
{
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8011
|
+
version: text,
|
|
8012
|
+
host: object2({ version: { type: ["string", "null"] }, range: text, tested: { type: "boolean" } }, ["version", "range", "tested"]),
|
|
8013
|
+
state: { enum: STATES },
|
|
7975
8014
|
gws: object2({ path: text, version: text }, ["path", "version"]),
|
|
7976
8015
|
services: texts,
|
|
7977
8016
|
accounts: { type: "array", items: account },
|
|
@@ -7980,7 +8019,7 @@ var status = object2(
|
|
|
7980
8019
|
permissionsBlock: text,
|
|
7981
8020
|
nextStep: text
|
|
7982
8021
|
},
|
|
7983
|
-
["
|
|
8022
|
+
["version", "host", "state", "gws", "services", "accounts", "unguardedAgents", "nextStep"]
|
|
7984
8023
|
);
|
|
7985
8024
|
var Gws = Rpc.define({
|
|
7986
8025
|
id: RPC_ID,
|
|
@@ -7998,7 +8037,8 @@ function connectionLabel(account2) {
|
|
|
7998
8037
|
return account2.mode === "interactive" ? "sign-in required" : "not configured";
|
|
7999
8038
|
}
|
|
8000
8039
|
function openCodeLabel(status2) {
|
|
8001
|
-
|
|
8040
|
+
const version2 = status2.host.version ?? "unknown";
|
|
8041
|
+
return status2.host.tested ? `${version2} (tested)` : `${version2} (host untested; supported range ${status2.host.range})`;
|
|
8002
8042
|
}
|
|
8003
8043
|
var DELEGATION_LABEL = "Needs domain-wide delegation";
|
|
8004
8044
|
function delegationText(account2) {
|
|
@@ -8100,10 +8140,11 @@ var Lifecycle = class {
|
|
|
8100
8140
|
// src/domain/plans.ts
|
|
8101
8141
|
import { randomBytes } from "node:crypto";
|
|
8102
8142
|
var MAX_PLANS = 512;
|
|
8103
|
-
function redactedPreview(
|
|
8104
|
-
const { method, recipients, destination, bodySummary, reason } =
|
|
8143
|
+
function redactedPreview(plan2) {
|
|
8144
|
+
const { method, recipients, recipientsUnknown, destination, bodySummary, reason } = plan2.preview;
|
|
8105
8145
|
const view = { method };
|
|
8106
8146
|
if (recipients && recipients.length > 0) view.recipients = [...recipients];
|
|
8147
|
+
if (recipientsUnknown !== void 0) view.recipientsUnknown = recipientsUnknown;
|
|
8107
8148
|
if (destination !== void 0) view.destination = destination;
|
|
8108
8149
|
if (bodySummary !== void 0) view.bodySummary = bodySummary;
|
|
8109
8150
|
if (reason !== void 0) view.reason = reason;
|
|
@@ -8115,14 +8156,14 @@ var PlanStore = class {
|
|
|
8115
8156
|
this.sweep();
|
|
8116
8157
|
const now = Date.now();
|
|
8117
8158
|
const { ttlSeconds, ...fields } = input;
|
|
8118
|
-
const
|
|
8159
|
+
const plan2 = {
|
|
8119
8160
|
...fields,
|
|
8120
8161
|
id: `plan_${randomBytes(18).toString("base64url")}`,
|
|
8121
8162
|
state: "ready",
|
|
8122
8163
|
createdAt: now,
|
|
8123
8164
|
expiresAt: now + ttlSeconds * 1e3
|
|
8124
8165
|
};
|
|
8125
|
-
this.plans.set(
|
|
8166
|
+
this.plans.set(plan2.id, plan2);
|
|
8126
8167
|
while (this.plans.size > MAX_PLANS) {
|
|
8127
8168
|
const oldest = this.plans.keys().next().value;
|
|
8128
8169
|
if (oldest === void 0) break;
|
|
@@ -8130,71 +8171,70 @@ var PlanStore = class {
|
|
|
8130
8171
|
if (evicted) this.expire(evicted);
|
|
8131
8172
|
this.plans.delete(oldest);
|
|
8132
8173
|
}
|
|
8133
|
-
return
|
|
8174
|
+
return plan2;
|
|
8134
8175
|
}
|
|
8135
8176
|
get(id) {
|
|
8136
|
-
const
|
|
8137
|
-
if (!
|
|
8138
|
-
if (
|
|
8139
|
-
return
|
|
8177
|
+
const plan2 = this.plans.get(id);
|
|
8178
|
+
if (!plan2) return void 0;
|
|
8179
|
+
if (plan2.state === "ready" && Date.now() >= plan2.expiresAt) this.expire(plan2);
|
|
8180
|
+
return plan2;
|
|
8140
8181
|
}
|
|
8141
8182
|
/** Ready (committable) plans, oldest first, optionally for one session. */
|
|
8142
8183
|
list(filter = {}) {
|
|
8143
8184
|
const ready = [];
|
|
8144
8185
|
for (const id of [...this.plans.keys()]) {
|
|
8145
|
-
const
|
|
8146
|
-
if (
|
|
8147
|
-
if (filter.sessionId !== void 0 &&
|
|
8148
|
-
ready.push(
|
|
8186
|
+
const plan2 = this.get(id);
|
|
8187
|
+
if (plan2?.state !== "ready") continue;
|
|
8188
|
+
if (filter.sessionId !== void 0 && plan2.sessionId !== filter.sessionId) continue;
|
|
8189
|
+
ready.push(plan2);
|
|
8149
8190
|
}
|
|
8150
8191
|
return ready.sort((a, b) => a.createdAt - b.createdAt);
|
|
8151
8192
|
}
|
|
8152
8193
|
/** Validate a commit binding without consuming the plan. */
|
|
8153
8194
|
check(id, binding) {
|
|
8154
|
-
const
|
|
8155
|
-
if (!
|
|
8156
|
-
if (
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
if (
|
|
8195
|
+
const plan2 = this.get(id);
|
|
8196
|
+
if (!plan2) throw new GwsError("PLAN_NOT_FOUND", "run gws_plan first");
|
|
8197
|
+
if (plan2.state === "consumed") {
|
|
8198
|
+
throw new GwsError("PLAN_ALREADY_USED", "this plan was already committed", { next: "Create a new plan to repeat the change; check the earlier result with gws_read first." });
|
|
8199
|
+
}
|
|
8200
|
+
if (plan2.state === "expired" || Date.now() >= plan2.expiresAt) throw new GwsError("PLAN_EXPIRED", "create a new plan");
|
|
8201
|
+
if (plan2.state === "committing") throw new GwsError("PLAN_ALREADY_USED", "plan is committing", { next: "Wait for the running commit to finish; do not commit this plan again." });
|
|
8202
|
+
if (plan2.sessionId !== binding.sessionId || plan2.agentId !== binding.agentId) {
|
|
8160
8203
|
throw new GwsError("PLAN_CALLER_MISMATCH", "a plan is bound to the session and agent that created it");
|
|
8161
8204
|
}
|
|
8162
|
-
if (
|
|
8205
|
+
if (plan2.category !== binding.category) {
|
|
8163
8206
|
throw new GwsError("CATEGORY_MISMATCH", "use the commit tool that matches the plan category", { next: "Commit with the tool named by the plan's category, passing only {planId}." });
|
|
8164
8207
|
}
|
|
8165
|
-
return
|
|
8208
|
+
return plan2;
|
|
8166
8209
|
}
|
|
8167
8210
|
acquire(id, binding) {
|
|
8168
|
-
const
|
|
8169
|
-
if (
|
|
8211
|
+
const plan2 = this.check(id, binding);
|
|
8212
|
+
if (plan2.account !== binding.account) {
|
|
8170
8213
|
throw new GwsError("PLAN_CALLER_MISMATCH", "a plan is bound to the account that created it");
|
|
8171
8214
|
}
|
|
8172
|
-
|
|
8173
|
-
return
|
|
8215
|
+
plan2.state = "committing";
|
|
8216
|
+
return plan2;
|
|
8174
8217
|
}
|
|
8175
8218
|
complete(id) {
|
|
8176
|
-
const
|
|
8177
|
-
if (
|
|
8178
|
-
|
|
8219
|
+
const plan2 = this.plans.get(id);
|
|
8220
|
+
if (plan2?.state !== "committing") return;
|
|
8221
|
+
plan2.state = "consumed";
|
|
8179
8222
|
}
|
|
8180
8223
|
release(id) {
|
|
8181
|
-
const
|
|
8182
|
-
if (
|
|
8224
|
+
const plan2 = this.plans.get(id);
|
|
8225
|
+
if (plan2 && plan2.state === "committing") this.expire(plan2);
|
|
8183
8226
|
}
|
|
8184
8227
|
invalidateAll() {
|
|
8185
|
-
for (const
|
|
8228
|
+
for (const plan2 of this.plans.values()) this.expire(plan2);
|
|
8186
8229
|
}
|
|
8187
|
-
expire(
|
|
8188
|
-
if (
|
|
8189
|
-
|
|
8230
|
+
expire(plan2) {
|
|
8231
|
+
if (plan2.state !== "ready" && plan2.state !== "committing") return;
|
|
8232
|
+
plan2.state = "expired";
|
|
8190
8233
|
}
|
|
8191
8234
|
sweep() {
|
|
8192
8235
|
const now = Date.now();
|
|
8193
|
-
for (const [id,
|
|
8194
|
-
if (
|
|
8195
|
-
if (plan.state === "consumed" || plan.state === "expired" || now >= plan.expiresAt) {
|
|
8196
|
-
this.plans.delete(id);
|
|
8197
|
-
}
|
|
8236
|
+
for (const [id, plan2] of this.plans) {
|
|
8237
|
+
if (now >= plan2.expiresAt && plan2.state !== "committing") this.plans.delete(id);
|
|
8198
8238
|
}
|
|
8199
8239
|
}
|
|
8200
8240
|
};
|
|
@@ -8203,11 +8243,11 @@ var PlanStore = class {
|
|
|
8203
8243
|
function isApiError(value) {
|
|
8204
8244
|
if (Array.isArray(value)) return value.some(isApiError);
|
|
8205
8245
|
if (!value || typeof value !== "object") return false;
|
|
8206
|
-
const
|
|
8207
|
-
return "error" in
|
|
8246
|
+
const record3 = value;
|
|
8247
|
+
return "error" in record3 && record3.error !== null && typeof record3.error === "object";
|
|
8208
8248
|
}
|
|
8209
|
-
function parseJson(
|
|
8210
|
-
const lines =
|
|
8249
|
+
function parseJson(text4) {
|
|
8250
|
+
const lines = text4.split("\n");
|
|
8211
8251
|
const start2 = lines.findIndex((line) => /^[[{]/.test(line.trim()));
|
|
8212
8252
|
if (start2 < 0) return void 0;
|
|
8213
8253
|
const candidate = lines.slice(start2).join("\n").trim();
|
|
@@ -8231,28 +8271,42 @@ function inlineCap(maxInlineBytes, mode, requested) {
|
|
|
8231
8271
|
return requested === void 0 ? base2 : Math.min(base2, requested);
|
|
8232
8272
|
}
|
|
8233
8273
|
function parseResult(result, options) {
|
|
8234
|
-
const
|
|
8235
|
-
const parsed = parseJson(
|
|
8236
|
-
const serialized = parsed === void 0 ?
|
|
8274
|
+
const text4 = result.stdout.toString("utf8");
|
|
8275
|
+
const parsed = parseJson(text4);
|
|
8276
|
+
const serialized = parsed === void 0 ? text4 : JSON.stringify(parsed);
|
|
8237
8277
|
const bytes2 = Buffer.byteLength(serialized);
|
|
8238
8278
|
const cap = inlineCap(options.maxInlineBytes, options.mode, options.requestedInlineBytes);
|
|
8239
8279
|
const truncated = bytes2 > cap;
|
|
8240
|
-
const empty =
|
|
8280
|
+
const empty = text4.trim().length === 0;
|
|
8241
8281
|
return {
|
|
8242
8282
|
// A successful DELETE commonly returns 204 with no body. Empty output
|
|
8243
8283
|
// with exit 0 is success; non-JSON text is not a JSON result.
|
|
8244
8284
|
ok: result.exitCode === 0 && (empty || parsed !== void 0 && !isApiError(parsed)),
|
|
8245
8285
|
data: parsed,
|
|
8246
|
-
raw: parsed === void 0 ?
|
|
8286
|
+
raw: parsed === void 0 ? text4.slice(0, cap) : void 0,
|
|
8247
8287
|
truncated,
|
|
8248
8288
|
diagnostics: { exitCode: result.exitCode, durationMs: result.durationMs, bytes: bytes2 }
|
|
8249
8289
|
};
|
|
8250
8290
|
}
|
|
8291
|
+
function withoutSavedBody(data, next) {
|
|
8292
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return data;
|
|
8293
|
+
const record3 = data;
|
|
8294
|
+
const keys = Object.keys(record3);
|
|
8295
|
+
if (typeof record3["saved_file"] !== "string" || typeof record3["bytes"] !== "number" || record3["status"] !== "success") return data;
|
|
8296
|
+
if (!keys.every((key) => ["bytes", "mimeType", "saved_file", "status"].includes(key))) return data;
|
|
8297
|
+
if (record3["bytes"] === 0) return "";
|
|
8298
|
+
return {
|
|
8299
|
+
...typeof record3["mimeType"] === "string" ? { mimeType: record3["mimeType"] } : {},
|
|
8300
|
+
bytes: record3["bytes"],
|
|
8301
|
+
saved: false,
|
|
8302
|
+
...next ? { next } : {}
|
|
8303
|
+
};
|
|
8304
|
+
}
|
|
8251
8305
|
function itemKey(page2) {
|
|
8252
8306
|
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return void 0;
|
|
8253
|
-
const
|
|
8254
|
-
if (Array.isArray(
|
|
8255
|
-
const arrays = Object.keys(
|
|
8307
|
+
const record3 = page2;
|
|
8308
|
+
if (Array.isArray(record3["items"])) return "items";
|
|
8309
|
+
const arrays = Object.keys(record3).filter((key) => Array.isArray(record3[key]));
|
|
8256
8310
|
return arrays.length === 1 ? arrays[0] : void 0;
|
|
8257
8311
|
}
|
|
8258
8312
|
function capPageItems(data, maxItems) {
|
|
@@ -8272,11 +8326,11 @@ function capPageItems(data, maxItems) {
|
|
|
8272
8326
|
kept.push(page2);
|
|
8273
8327
|
continue;
|
|
8274
8328
|
}
|
|
8275
|
-
const
|
|
8276
|
-
const list =
|
|
8329
|
+
const record3 = page2;
|
|
8330
|
+
const list = record3[key];
|
|
8277
8331
|
const room = maxItems - items;
|
|
8278
8332
|
if (list.length > room) {
|
|
8279
|
-
const { nextPageToken: _dropped, ...rest } =
|
|
8333
|
+
const { nextPageToken: _dropped, ...rest } = record3;
|
|
8280
8334
|
kept.push({ ...rest, [key]: list.slice(0, room) });
|
|
8281
8335
|
items += room;
|
|
8282
8336
|
capped = true;
|
|
@@ -8315,29 +8369,33 @@ function stderrText(result) {
|
|
|
8315
8369
|
var EXIT = { auth: 2, validation: 3, discovery: 4, internal: 5 };
|
|
8316
8370
|
var SCOPE_REASONS = /* @__PURE__ */ new Set(["ACCESS_TOKEN_SCOPE_INSUFFICIENT", "insufficientPermissions"]);
|
|
8317
8371
|
var PRIVILEGE_REFUSALS = /\bnot authori[sz]ed to (?:read|access|view|list|use|manage|perform)\b|\bcaller does not have access to the customers? reporting data\b/i;
|
|
8318
|
-
function privilegeRefused(result, parsed) {
|
|
8372
|
+
function privilegeRefused(result, service, parsed) {
|
|
8373
|
+
if (!isAdminService(service)) return false;
|
|
8319
8374
|
if (result.timedOut || result.outputCapped || result.exitCode === 0 || result.exitCode === EXIT.auth) return false;
|
|
8320
8375
|
const error2 = apiErrorOf(result, parsed);
|
|
8321
8376
|
if (error2?.code !== void 0 && error2.code !== 401 && error2.code !== 403) return false;
|
|
8322
|
-
const
|
|
8323
|
-
return !/invalid_grant/i.test(
|
|
8377
|
+
const text4 = [error2?.message, stderrText(result)].filter(Boolean).join("\n");
|
|
8378
|
+
return !/invalid_grant/i.test(text4) && PRIVILEGE_REFUSALS.test(text4);
|
|
8379
|
+
}
|
|
8380
|
+
function isAdminService(service) {
|
|
8381
|
+
return service.startsWith("admin-");
|
|
8324
8382
|
}
|
|
8325
8383
|
var ADMIN_PRIVILEGES = { "admin-reports": "Reports" };
|
|
8326
8384
|
function privilegeName(service) {
|
|
8327
8385
|
const name = service ? ADMIN_PRIVILEGES[service] : void 0;
|
|
8328
8386
|
return name ? `the ${name} privilege` : "the admin privilege it needs";
|
|
8329
8387
|
}
|
|
8330
|
-
function authProblem(result, parsed) {
|
|
8388
|
+
function authProblem(result, service, parsed) {
|
|
8331
8389
|
if (result.timedOut || result.outputCapped) return void 0;
|
|
8332
|
-
if (privilegeRefused(result, parsed)) return void 0;
|
|
8390
|
+
if (privilegeRefused(result, service, parsed)) return void 0;
|
|
8333
8391
|
const error2 = apiErrorOf(result, parsed);
|
|
8334
8392
|
if (result.exitCode === 0 && !error2) return void 0;
|
|
8335
8393
|
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
8336
|
-
const
|
|
8337
|
-
if (reasons.some((reason) => reason !== void 0 && SCOPE_REASONS.has(reason)) || /insufficient authentication scopes|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(
|
|
8394
|
+
const text4 = [error2?.message, error2?.status, stderrText(result)].filter(Boolean).join("\n");
|
|
8395
|
+
if (reasons.some((reason) => reason !== void 0 && SCOPE_REASONS.has(reason)) || /insufficient authentication scopes|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(text4)) {
|
|
8338
8396
|
return "AUTH_SCOPE_MISSING";
|
|
8339
8397
|
}
|
|
8340
|
-
if (error2?.code === 401 || error2?.status === "UNAUTHENTICATED" || reasons.includes("authError") || /invalid_grant|\bUNAUTHENTICATED\b|^error\[auth\]/im.test(
|
|
8398
|
+
if (error2?.code === 401 || error2?.status === "UNAUTHENTICATED" || reasons.includes("authError") || /invalid_grant|\bUNAUTHENTICATED\b|^error\[auth\]/im.test(text4)) {
|
|
8341
8399
|
return "AUTH_REQUIRED";
|
|
8342
8400
|
}
|
|
8343
8401
|
return void 0;
|
|
@@ -8352,28 +8410,28 @@ var CONSOLE_URL = /https:\/\/console\.(?:developers|cloud)\.google\.com\/[^\s"'<
|
|
|
8352
8410
|
function disabledApi(result, parsed) {
|
|
8353
8411
|
const error2 = apiErrorOf(result, parsed);
|
|
8354
8412
|
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
8355
|
-
const
|
|
8356
|
-
if (!reasons.some((reason) => reason !== void 0 && DISABLED_REASONS.has(reason)) && !/accessnotconfigured|has not been used|is disabled/i.test(
|
|
8413
|
+
const text4 = [error2?.message, stderrText(result)].filter(Boolean).join("\n");
|
|
8414
|
+
if (!reasons.some((reason) => reason !== void 0 && DISABLED_REASONS.has(reason)) && !/accessnotconfigured|has not been used|is disabled/i.test(text4)) {
|
|
8357
8415
|
return void 0;
|
|
8358
8416
|
}
|
|
8359
|
-
const enableUrl = (typeof error2?.enable_url === "string" && CONSOLE_URL.test(error2.enable_url) ? error2.enable_url : CONSOLE_URL.exec(
|
|
8360
|
-
const project = (enableUrl ? /[?&]project=([\w.:-]+)/.exec(enableUrl)?.[1] : void 0) ?? /\bin project ([\w.:-]+)/.exec(
|
|
8417
|
+
const enableUrl = (typeof error2?.enable_url === "string" && CONSOLE_URL.test(error2.enable_url) ? error2.enable_url : CONSOLE_URL.exec(text4)?.[0])?.replace(/[.,;]+$/, "");
|
|
8418
|
+
const project = (enableUrl ? /[?&]project=([\w.:-]+)/.exec(enableUrl)?.[1] : void 0) ?? /\bin project ([\w.:-]+)/.exec(text4)?.[1];
|
|
8361
8419
|
return { ...enableUrl ? { enableUrl } : {}, ...project ? { project } : {} };
|
|
8362
8420
|
}
|
|
8363
|
-
function processError(result, parsed, remediation = GENERIC_REMEDIATION) {
|
|
8421
|
+
function processError(result, parsed, service, remediation = GENERIC_REMEDIATION) {
|
|
8364
8422
|
if (result.timedOut) return new GwsError("TIMEOUT", `gws did not finish within the configured execution.timeoutMs (${result.durationMs}ms elapsed)`);
|
|
8365
8423
|
if (result.outputCapped) return new GwsError("OUTPUT_LIMIT", "gws output exceeded execution.maxStdoutBytes/maxStderrBytes; narrow fields or pagination");
|
|
8366
8424
|
const stderr = stderrText(result);
|
|
8367
8425
|
const payload = apiErrorOf(result, parsed);
|
|
8368
8426
|
const message = payload?.message || stderr || "gws command failed";
|
|
8369
|
-
if (privilegeRefused(result, parsed)) {
|
|
8427
|
+
if (privilegeRefused(result, service, parsed)) {
|
|
8370
8428
|
return new GwsError(
|
|
8371
8429
|
"ADMIN_PRIVILEGE_REQUIRED",
|
|
8372
8430
|
`Google accepted the sign-in but refused the call, because the account lacks a Workspace admin privilege; signing in again as the same account cannot help (gws: ${message.slice(0, 300)})`,
|
|
8373
8431
|
{ next: `${remediation.privilege.charAt(0).toUpperCase()}${remediation.privilege.slice(1)}.` }
|
|
8374
8432
|
);
|
|
8375
8433
|
}
|
|
8376
|
-
const problem = authProblem(result, parsed) ?? (result.exitCode === EXIT.auth ? "AUTH_REQUIRED" : void 0);
|
|
8434
|
+
const problem = authProblem(result, service, parsed) ?? (result.exitCode === EXIT.auth ? "AUTH_REQUIRED" : void 0);
|
|
8377
8435
|
if (problem === "AUTH_SCOPE_MISSING") {
|
|
8378
8436
|
return new GwsError("AUTH_SCOPE_MISSING", `the credential lacks a scope for this operation; ${remediation.consent} (gws: ${message.slice(0, 300)})`);
|
|
8379
8437
|
}
|
|
@@ -8397,16 +8455,16 @@ var COMMANDS = {
|
|
|
8397
8455
|
status: { name: "gws-status", description: "Google Workspace: sign-in, access, pending plans, setup steps, and unguarded agents" },
|
|
8398
8456
|
doctor: { name: "gws-doctor", description: "Google Workspace: check every account's services with one small read each" }
|
|
8399
8457
|
};
|
|
8400
|
-
function planSummary(
|
|
8458
|
+
function planSummary(plan2) {
|
|
8401
8459
|
return {
|
|
8402
|
-
planId:
|
|
8403
|
-
sessionID:
|
|
8404
|
-
category:
|
|
8405
|
-
operation:
|
|
8406
|
-
account:
|
|
8407
|
-
preview: redactedPreview(
|
|
8408
|
-
createdAt: new Date(
|
|
8409
|
-
expiresAt: new Date(
|
|
8460
|
+
planId: plan2.id,
|
|
8461
|
+
sessionID: plan2.sessionId,
|
|
8462
|
+
category: plan2.category,
|
|
8463
|
+
operation: plan2.operation,
|
|
8464
|
+
account: plan2.account,
|
|
8465
|
+
preview: redactedPreview(plan2),
|
|
8466
|
+
createdAt: new Date(plan2.createdAt).toISOString(),
|
|
8467
|
+
expiresAt: new Date(plan2.expiresAt).toISOString()
|
|
8410
8468
|
};
|
|
8411
8469
|
}
|
|
8412
8470
|
function summarizeStatus(status2, plans = []) {
|
|
@@ -8465,7 +8523,7 @@ function formatStatus(status2, plans = [], now = Date.now()) {
|
|
|
8465
8523
|
lines.push(`Agents without default rules: ${status2.unguardedAgents.join(", ")}`, status2.permissionsBlock ?? "", BLOCK_PLACEMENT);
|
|
8466
8524
|
}
|
|
8467
8525
|
if (status2.auditDegraded) lines.push("Audit: some records could not be saved, so the audit log may be incomplete.");
|
|
8468
|
-
lines.push(`OpenCode: ${openCodeLabel(status2)}`, `Next step: ${status2.nextStep
|
|
8526
|
+
lines.push(`OpenCode: ${openCodeLabel(status2)}`, `Next step: ${status2.nextStep}`);
|
|
8469
8527
|
return lines.join("\n");
|
|
8470
8528
|
}
|
|
8471
8529
|
function planLines(plans, now) {
|
|
@@ -8474,14 +8532,15 @@ function planLines(plans, now) {
|
|
|
8474
8532
|
lines.push("No pending plans in this session.");
|
|
8475
8533
|
} else {
|
|
8476
8534
|
lines.push(`Pending plans in this session (${plans.length}):`);
|
|
8477
|
-
for (const
|
|
8535
|
+
for (const plan2 of plans) {
|
|
8478
8536
|
const details = [
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
`
|
|
8537
|
+
plan2.preview.recipients?.length ? `to ${plan2.preview.recipients.join(", ")}` : void 0,
|
|
8538
|
+
plan2.preview.recipientsUnknown ? `recipients unknown: ${plan2.preview.recipientsUnknown}` : void 0,
|
|
8539
|
+
plan2.preview.destination ? `destination ${plan2.preview.destination}` : void 0,
|
|
8540
|
+
plan2.preview.bodySummary ? `body ${plan2.preview.bodySummary}` : void 0,
|
|
8541
|
+
`expires ${remaining(plan2.expiresAt, now)}`
|
|
8483
8542
|
].filter(Boolean);
|
|
8484
|
-
lines.push(`- ${
|
|
8543
|
+
lines.push(`- ${plan2.category} ${plan2.operation} as ${plan2.account}: ${details.join(" \xB7 ")} (${plan2.planId})`);
|
|
8485
8544
|
}
|
|
8486
8545
|
lines.push("Approval happens only in the OpenCode permission prompt when the agent commits a plan. Pending plans expire automatically.");
|
|
8487
8546
|
}
|
|
@@ -8530,8 +8589,8 @@ function formatDoctor(outcomes) {
|
|
|
8530
8589
|
if (outcomes.length === 0) lines.push("No account enables a service.");
|
|
8531
8590
|
for (const outcome of outcomes) {
|
|
8532
8591
|
const next = nextFor(outcome);
|
|
8533
|
-
const
|
|
8534
|
-
lines.push(`- ${outcome.account} ${outcome.service}: ${outcome.status}${
|
|
8592
|
+
const text4 = [outcome.detail, next ? `Next step: ${next.charAt(0).toUpperCase()}${next.slice(1)}` : void 0].filter(Boolean).join(". ");
|
|
8593
|
+
lines.push(`- ${outcome.account} ${outcome.service}: ${outcome.status}${text4 ? ` \xB7 ${text4}` : ""}`);
|
|
8535
8594
|
}
|
|
8536
8595
|
return lines.join("\n");
|
|
8537
8596
|
}
|
|
@@ -8542,9 +8601,9 @@ function summarizeDoctor(outcomes) {
|
|
|
8542
8601
|
}
|
|
8543
8602
|
async function registerCommands(ctx, sources, signal) {
|
|
8544
8603
|
const lifetime = new Lifecycle();
|
|
8545
|
-
const post = (sessionID, delivery,
|
|
8604
|
+
const post = (sessionID, delivery, text4, description, command) => lifetime.wait(() => ctx.session.synthetic({
|
|
8546
8605
|
sessionID,
|
|
8547
|
-
text:
|
|
8606
|
+
text: text4,
|
|
8548
8607
|
description,
|
|
8549
8608
|
metadata: { source: "opencode-gws", command },
|
|
8550
8609
|
delivery,
|
|
@@ -8896,8 +8955,8 @@ function tokenError(status2, json, secrets) {
|
|
|
8896
8955
|
message = scrubSecrets(message, secrets);
|
|
8897
8956
|
return new Error(message);
|
|
8898
8957
|
}
|
|
8899
|
-
function scrubSecrets(
|
|
8900
|
-
let out =
|
|
8958
|
+
function scrubSecrets(text4, secrets) {
|
|
8959
|
+
let out = text4;
|
|
8901
8960
|
for (const secret of secrets) {
|
|
8902
8961
|
if (secret) out = out.split(secret).join("[redacted]");
|
|
8903
8962
|
}
|
|
@@ -8919,15 +8978,15 @@ async function postToken(endpoint, body, fetchImpl, secrets) {
|
|
|
8919
8978
|
} catch {
|
|
8920
8979
|
throw new Error("Google token request failed: could not reach the token endpoint.");
|
|
8921
8980
|
}
|
|
8922
|
-
let
|
|
8981
|
+
let text4 = "";
|
|
8923
8982
|
try {
|
|
8924
|
-
|
|
8983
|
+
text4 = await response.text();
|
|
8925
8984
|
} catch {
|
|
8926
8985
|
}
|
|
8927
8986
|
let json = {};
|
|
8928
|
-
if (
|
|
8987
|
+
if (text4.trim()) {
|
|
8929
8988
|
try {
|
|
8930
|
-
const parsed = JSON.parse(
|
|
8989
|
+
const parsed = JSON.parse(text4);
|
|
8931
8990
|
if (isRecord(parsed)) json = parsed;
|
|
8932
8991
|
} catch {
|
|
8933
8992
|
if (response.ok) throw new Error("Google token endpoint returned an invalid response.");
|
|
@@ -9387,22 +9446,27 @@ function nextStep(accounts, agents) {
|
|
|
9387
9446
|
if (agents.length > 0) return "Put the permission block first in the own permissions of each agent without default rules, followed by your top-level rules for gws actions.";
|
|
9388
9447
|
return void 0;
|
|
9389
9448
|
}
|
|
9449
|
+
function stateOf(accounts, tested) {
|
|
9450
|
+
if (accounts.some((account2) => account2.needsAuth || !account2.connected && account2.mode === "interactive")) return "sign_in_required";
|
|
9451
|
+
if (accounts.some((account2) => !account2.connected)) return "not_configured";
|
|
9452
|
+
return tested ? "connected" : "host_untested";
|
|
9453
|
+
}
|
|
9390
9454
|
function statusSource(runtime, listAgents) {
|
|
9391
9455
|
return async (signal) => {
|
|
9392
9456
|
const base2 = await runtime.status(signal);
|
|
9393
9457
|
const agents = unguardedAgents(await runtime.wait(() => listAgents(), signal));
|
|
9394
9458
|
const next = nextStep(base2.accounts, agents);
|
|
9395
9459
|
return {
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9460
|
+
version: base2.version,
|
|
9461
|
+
host: base2.host,
|
|
9462
|
+
state: stateOf(base2.accounts, base2.host.tested),
|
|
9399
9463
|
gws: { path: base2.binary.path, version: base2.binary.version },
|
|
9400
9464
|
services: base2.services,
|
|
9401
9465
|
accounts: base2.accounts,
|
|
9402
9466
|
auditDegraded: base2.auditDegraded,
|
|
9403
9467
|
unguardedAgents: agents,
|
|
9404
9468
|
...agents.length > 0 ? { permissionsBlock: permissionsBlock(agents) } : {},
|
|
9405
|
-
|
|
9469
|
+
nextStep: next ?? NOTHING_TO_DO
|
|
9406
9470
|
};
|
|
9407
9471
|
};
|
|
9408
9472
|
}
|
|
@@ -9447,25 +9511,25 @@ function operationRequest(call2, action, target) {
|
|
|
9447
9511
|
metadata: compact({ ...mark(call2), account: target.account, operation: target.operation, service: target.service, category: target.category })
|
|
9448
9512
|
};
|
|
9449
9513
|
}
|
|
9450
|
-
function planMetadata(
|
|
9514
|
+
function planMetadata(plan2) {
|
|
9451
9515
|
return compact({
|
|
9452
|
-
planId:
|
|
9453
|
-
category:
|
|
9454
|
-
operation:
|
|
9455
|
-
account:
|
|
9456
|
-
...redactedPreview(
|
|
9457
|
-
expiresAt: new Date(
|
|
9516
|
+
planId: plan2.id,
|
|
9517
|
+
category: plan2.category,
|
|
9518
|
+
operation: plan2.operation,
|
|
9519
|
+
account: plan2.account,
|
|
9520
|
+
...redactedPreview(plan2),
|
|
9521
|
+
expiresAt: new Date(plan2.expiresAt).toISOString()
|
|
9458
9522
|
});
|
|
9459
9523
|
}
|
|
9460
9524
|
var SAVABLE = /* @__PURE__ */ new Set(["write", "export"]);
|
|
9461
|
-
function commitRequests(call2, action,
|
|
9462
|
-
const id = resource(
|
|
9463
|
-
const request = { ...base(call2, action), resources: [id], metadata: planMetadata(
|
|
9464
|
-
if (SAVABLE.has(
|
|
9525
|
+
function commitRequests(call2, action, plan2) {
|
|
9526
|
+
const id = resource(plan2.account, plan2.operation);
|
|
9527
|
+
const request = { ...base(call2, action), resources: [id], metadata: planMetadata(plan2) };
|
|
9528
|
+
if (SAVABLE.has(plan2.category)) request.save = [id];
|
|
9465
9529
|
return [request];
|
|
9466
9530
|
}
|
|
9467
|
-
function compact(
|
|
9468
|
-
return Object.fromEntries(Object.entries(
|
|
9531
|
+
function compact(record3) {
|
|
9532
|
+
return Object.fromEntries(Object.entries(record3).filter(([, value]) => value !== void 0));
|
|
9469
9533
|
}
|
|
9470
9534
|
|
|
9471
9535
|
// src/host/tools.ts
|
|
@@ -9608,7 +9672,8 @@ var Preview = object({
|
|
|
9608
9672
|
url: string2().nullable().optional(),
|
|
9609
9673
|
params: JsonObject.describe("Parameters with secret-like values redacted"),
|
|
9610
9674
|
bodySummary: string2().optional().describe("Body keys and size; never the body"),
|
|
9611
|
-
recipients: array(string2()).optional(),
|
|
9675
|
+
recipients: array(string2()).optional().describe("Every address a send or share reaches, including those read for a reply, a draft or an event; *@domain for a whole domain"),
|
|
9676
|
+
recipientsUnknown: string2().optional().describe("Why the plugin could not tell everyone this send or share reaches"),
|
|
9612
9677
|
uploads: array(string2()).optional().describe("Local files the commit uploads or attaches"),
|
|
9613
9678
|
destination: string2().optional().describe("Local file the export writes"),
|
|
9614
9679
|
overwrite: boolean2().optional().describe("The export replaces an existing file at destination"),
|
|
@@ -9649,17 +9714,23 @@ var AccountStatus = object({
|
|
|
9649
9714
|
delegationRequired: array(string2()).optional().describe("Enabled services that need domain-wide delegation, which a Workspace admin sets up; the sign-in requests none of their scopes unless accounts.<alias>.scopes adds them"),
|
|
9650
9715
|
nextStep: string2().optional().describe("What to do next; absent when nothing is needed")
|
|
9651
9716
|
});
|
|
9717
|
+
var Host = object({
|
|
9718
|
+
version: string2().nullable().describe("The OpenCode version; null when unknown"),
|
|
9719
|
+
range: string2().describe("The supported range, as in package.json opencode.host"),
|
|
9720
|
+
tested: boolean2().describe("True only for a release the plugin is tested on")
|
|
9721
|
+
});
|
|
9722
|
+
var STATES2 = ["connected", "sign_in_required", "not_configured", "host_untested"];
|
|
9652
9723
|
var StatusOutput = object({
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9724
|
+
version: string2().describe("The plugin's version"),
|
|
9725
|
+
host: Host,
|
|
9726
|
+
state: _enum(STATES2).describe("connected; sign_in_required or not_configured when an account needs it; host_untested on an untested OpenCode"),
|
|
9656
9727
|
gws: object({ path: string2(), version: string2() }),
|
|
9657
9728
|
services: array(string2()).describe("Model-facing services"),
|
|
9658
9729
|
accounts: array(AccountStatus),
|
|
9659
9730
|
auditDegraded: boolean2().describe("Audit records were dropped, failed, or exceeded the sink deadline; sticky until reload"),
|
|
9660
9731
|
unguardedAgents: number2().int().describe("How many agents missed the default ask rules"),
|
|
9661
9732
|
permissions: string2().optional().describe("Present when an agent missed the ask rules: where to find the fix"),
|
|
9662
|
-
nextStep: string2().
|
|
9733
|
+
nextStep: string2().describe('The one thing to do next; "Nothing to do." when nothing is needed')
|
|
9663
9734
|
});
|
|
9664
9735
|
|
|
9665
9736
|
// src/host/tools.ts
|
|
@@ -9763,6 +9834,7 @@ function toolSpecs(runtime, assert2, status2) {
|
|
|
9763
9834
|
report(call2, input.operation);
|
|
9764
9835
|
const target = runtime.inspect(input, "plan");
|
|
9765
9836
|
await authorize(call2, operationRequest(call2.host, "gws.plan", target));
|
|
9837
|
+
for (const operation of target.reads ?? []) await authorize(call2, operationRequest(call2.host, "gws.read", { ...target, operation, category: "read" }));
|
|
9766
9838
|
const output = await runtime.plan(input, call2.runtime);
|
|
9767
9839
|
return { output, metadata: { operation: target.operation, category: output.category, account: target.account } };
|
|
9768
9840
|
}
|
|
@@ -9776,11 +9848,11 @@ function toolSpecs(runtime, assert2, status2) {
|
|
|
9776
9848
|
input: CommitInput,
|
|
9777
9849
|
output: CommitOutput,
|
|
9778
9850
|
run: async (input, call2) => {
|
|
9779
|
-
const
|
|
9780
|
-
report(call2,
|
|
9781
|
-
for (const request of commitRequests(call2.host, `gws.${category}`,
|
|
9851
|
+
const plan2 = runtime.inspectPlan(category, input, call2.runtime);
|
|
9852
|
+
report(call2, plan2.operation);
|
|
9853
|
+
for (const request of commitRequests(call2.host, `gws.${category}`, plan2)) await authorize(call2, request);
|
|
9782
9854
|
const output = await runtime.commit(category, input, call2.runtime);
|
|
9783
|
-
return { output, metadata: { operation:
|
|
9855
|
+
return { output, metadata: { operation: plan2.operation, category, account: plan2.account } };
|
|
9784
9856
|
}
|
|
9785
9857
|
})
|
|
9786
9858
|
)
|
|
@@ -9857,28 +9929,51 @@ function report(call2, operation) {
|
|
|
9857
9929
|
call2.runtime.progress?.({ status: "resolving", operation });
|
|
9858
9930
|
}
|
|
9859
9931
|
function render(output, maxBytes) {
|
|
9860
|
-
const
|
|
9861
|
-
const bytes2 = Buffer.byteLength(
|
|
9862
|
-
if (bytes2 <= maxBytes) return { text:
|
|
9863
|
-
const cut = Buffer.from(
|
|
9932
|
+
const text4 = JSON.stringify(output) ?? "";
|
|
9933
|
+
const bytes2 = Buffer.byteLength(text4);
|
|
9934
|
+
if (bytes2 <= maxBytes) return { text: text4, truncated: false };
|
|
9935
|
+
const cut = Buffer.from(text4).subarray(0, maxBytes).toString("utf8");
|
|
9864
9936
|
return {
|
|
9865
9937
|
text: `${cut}
|
|
9866
9938
|
\u2026[truncated: ${maxBytes} of ${bytes2} bytes shown; narrow fields, params or pagination to see the rest]`,
|
|
9867
9939
|
truncated: true
|
|
9868
9940
|
};
|
|
9869
9941
|
}
|
|
9870
|
-
function compact2(
|
|
9871
|
-
return Object.fromEntries(Object.entries(
|
|
9942
|
+
function compact2(record3) {
|
|
9943
|
+
return Object.fromEntries(Object.entries(record3).filter(([, value]) => value !== void 0));
|
|
9872
9944
|
}
|
|
9873
9945
|
|
|
9874
9946
|
// src/host/scripting.ts
|
|
9875
9947
|
var TOOLS_RPC_ID = `${RPC_ID}.tools`;
|
|
9876
9948
|
var COMMIT_TOOLS = /* @__PURE__ */ new Set(["write", "send", "share", "delete", "export", "admin"]);
|
|
9877
9949
|
var UNATTENDED = "A script cannot answer an approval prompt; this needs an allow rule for the session's agent.";
|
|
9878
|
-
var
|
|
9879
|
-
sessionID:
|
|
9880
|
-
|
|
9881
|
-
|
|
9950
|
+
var ENVELOPE_MESSAGES = {
|
|
9951
|
+
notObject: `the envelope must be an object, such as {"sessionID":"<session>","input":<the tool's input>}`,
|
|
9952
|
+
unknownKey: (key) => `"${key}" is not allowed`,
|
|
9953
|
+
sessionID: `"sessionID" must be a non-empty string`,
|
|
9954
|
+
inputMissing: `"input" is missing`,
|
|
9955
|
+
describe: "describe takes no input"
|
|
9956
|
+
};
|
|
9957
|
+
var plainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9958
|
+
function strict(schema, validate2) {
|
|
9959
|
+
return Object.defineProperty({ ...schema }, "~standard", { value: { version: 1, vendor: "opencode-gws", validate: validate2 }, enumerable: false });
|
|
9960
|
+
}
|
|
9961
|
+
var ENVELOPE = strict(
|
|
9962
|
+
{ type: "object", properties: { sessionID: { type: "string", minLength: 1 }, input: {} }, required: ["sessionID", "input"], additionalProperties: false },
|
|
9963
|
+
(value) => {
|
|
9964
|
+
if (!plainObject(value)) return { issues: [{ message: ENVELOPE_MESSAGES.notObject }] };
|
|
9965
|
+
const found = [
|
|
9966
|
+
...Object.keys(value).filter((key) => key !== "sessionID" && key !== "input").map(ENVELOPE_MESSAGES.unknownKey),
|
|
9967
|
+
...typeof value["sessionID"] === "string" && value["sessionID"] !== "" ? [] : [ENVELOPE_MESSAGES.sessionID],
|
|
9968
|
+
..."input" in value ? [] : [ENVELOPE_MESSAGES.inputMissing]
|
|
9969
|
+
];
|
|
9970
|
+
return found.length === 0 ? { value } : { issues: found.map((message) => ({ message })) };
|
|
9971
|
+
}
|
|
9972
|
+
);
|
|
9973
|
+
var DESCRIBE_INPUT = strict(
|
|
9974
|
+
{ type: "object", properties: {}, additionalProperties: false },
|
|
9975
|
+
(value) => value === void 0 || value === null || plainObject(value) && Object.keys(value).length === 0 ? { value } : { issues: [{ message: ENVELOPE_MESSAGES.describe }] }
|
|
9976
|
+
);
|
|
9882
9977
|
var Failure = strictObject({ code: string2(), next: string2() });
|
|
9883
9978
|
var Description = strictObject({
|
|
9884
9979
|
id: string2(),
|
|
@@ -9895,9 +9990,9 @@ var Description = strictObject({
|
|
|
9895
9990
|
});
|
|
9896
9991
|
function sessionNotFound(error2) {
|
|
9897
9992
|
if (error2 === null || typeof error2 !== "object") return false;
|
|
9898
|
-
const
|
|
9899
|
-
const tag =
|
|
9900
|
-
return tag === "Session.NotFoundError" || tag === "SessionNotFoundError" ||
|
|
9993
|
+
const record3 = error2;
|
|
9994
|
+
const tag = record3["_tag"] ?? record3["name"];
|
|
9995
|
+
return tag === "Session.NotFoundError" || tag === "SessionNotFoundError" || record3["status"] === 404 || record3["statusCode"] === 404;
|
|
9901
9996
|
}
|
|
9902
9997
|
async function registerScripting(ctx, runtime, status2) {
|
|
9903
9998
|
const assert2 = async (request) => {
|
|
@@ -9937,8 +10032,8 @@ async function registerScripting(ctx, runtime, status2) {
|
|
|
9937
10032
|
const contract = Rpc2.define({
|
|
9938
10033
|
id: TOOLS_RPC_ID,
|
|
9939
10034
|
methods: {
|
|
9940
|
-
describe: { input:
|
|
9941
|
-
...Object.fromEntries(specs.map((spec2) => [spec2.name, { input:
|
|
10035
|
+
describe: { input: DESCRIBE_INPUT, output: Description },
|
|
10036
|
+
...Object.fromEntries(specs.map((spec2) => [spec2.name, { input: ENVELOPE, output: spec2.output, errors: { failed: Failure } }]))
|
|
9942
10037
|
},
|
|
9943
10038
|
events: {}
|
|
9944
10039
|
});
|
|
@@ -10041,12 +10136,12 @@ async function registerSkills(ctx, config2, visibleServices) {
|
|
|
10041
10136
|
}
|
|
10042
10137
|
|
|
10043
10138
|
// src/runtime/index.ts
|
|
10044
|
-
import { mkdtempSync, rmSync
|
|
10045
|
-
import { tmpdir } from "node:os";
|
|
10046
|
-
import { join as
|
|
10139
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
|
|
10140
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
10141
|
+
import { join as join9 } from "node:path";
|
|
10047
10142
|
|
|
10048
10143
|
// src/domain/audit.ts
|
|
10049
|
-
import {
|
|
10144
|
+
import { mkdir, rename, stat, writeFile } from "node:fs/promises";
|
|
10050
10145
|
import { write } from "node:fs";
|
|
10051
10146
|
import { dirname as dirname3 } from "node:path";
|
|
10052
10147
|
var EVENTS = /* @__PURE__ */ new Set(["plugin.loaded", "operation.read", "helper.read", "operation.plan", "operation.commit"]);
|
|
@@ -10055,6 +10150,7 @@ var UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
|
|
10055
10150
|
function auditErrorCode(error2) {
|
|
10056
10151
|
return GwsError.from(error2)?.code ?? UNKNOWN_ERROR;
|
|
10057
10152
|
}
|
|
10153
|
+
var AUDIT_MAX_BYTES = 5 * 1024 * 1024;
|
|
10058
10154
|
var MAX_QUEUE = 64;
|
|
10059
10155
|
var WRITE_TIMEOUT_MS = 100;
|
|
10060
10156
|
var Audit = class {
|
|
@@ -10065,6 +10161,9 @@ var Audit = class {
|
|
|
10065
10161
|
if (this.config.sink === "jsonl" && this.config.path) {
|
|
10066
10162
|
await mkdir(dirname3(this.config.path), { recursive: true, mode: 448 });
|
|
10067
10163
|
if (signal.aborted) return;
|
|
10164
|
+
const size = await stat(this.config.path).then((found) => found.size, () => 0);
|
|
10165
|
+
if (size > 0 && size + Buffer.byteLength(line) > AUDIT_MAX_BYTES) await rename(this.config.path, `${this.config.path}.1`);
|
|
10166
|
+
if (signal.aborted) return;
|
|
10068
10167
|
await writeFile(this.config.path, line, { flag: "a", mode: 384, signal });
|
|
10069
10168
|
} else {
|
|
10070
10169
|
await writeStderr(line, signal);
|
|
@@ -10184,9 +10283,9 @@ function assertTrusted(path, projectDirectory) {
|
|
|
10184
10283
|
if (!isAbsolute4(path)) throw new Error(`BINARY_TRUST_FAILED: Binary path must be absolute: ${path}`);
|
|
10185
10284
|
if (!existsSync2(path)) throw new Error(`GWS_UNAVAILABLE: The binary.path option names a missing file: ${path}. Next step: S${OVERRIDE.slice(1)}.`);
|
|
10186
10285
|
const real = realpathSync(path);
|
|
10187
|
-
const
|
|
10188
|
-
if (!
|
|
10189
|
-
if ((
|
|
10286
|
+
const stat3 = statSync(real);
|
|
10287
|
+
if (!stat3.isFile()) throw new Error(`BINARY_TRUST_FAILED: Not a regular file: ${real}`);
|
|
10288
|
+
if ((stat3.mode & 73) === 0 || (stat3.mode & 18) !== 0) {
|
|
10190
10289
|
throw new Error("BINARY_TRUST_FAILED: Binary must be executable and not group/world writable");
|
|
10191
10290
|
}
|
|
10192
10291
|
if (projectDirectory && isUnder(resolve(projectDirectory), real)) {
|
|
@@ -10197,13 +10296,13 @@ function assertTrusted(path, projectDirectory) {
|
|
|
10197
10296
|
if (typeof process.getgroups === "function") for (const gid of process.getgroups()) groups.add(gid);
|
|
10198
10297
|
let dir = dirname4(real);
|
|
10199
10298
|
for (; ; ) {
|
|
10200
|
-
const
|
|
10201
|
-
const mode =
|
|
10299
|
+
const stat4 = statSync(dir);
|
|
10300
|
+
const mode = stat4.mode & 511;
|
|
10202
10301
|
if ((mode & 2) !== 0) {
|
|
10203
10302
|
throw new Error(`BINARY_TRUST_FAILED: World-writable ancestor: ${dir} (mode ${mode.toString(8)})`);
|
|
10204
10303
|
}
|
|
10205
|
-
if ((mode & 16) !== 0 && !groups.has(
|
|
10206
|
-
throw new Error(`BINARY_TRUST_FAILED: Group-writable ancestor owned by a foreign group: ${dir} (mode ${mode.toString(8)}, gid ${
|
|
10304
|
+
if ((mode & 16) !== 0 && !groups.has(stat4.gid)) {
|
|
10305
|
+
throw new Error(`BINARY_TRUST_FAILED: Group-writable ancestor owned by a foreign group: ${dir} (mode ${mode.toString(8)}, gid ${stat4.gid})`);
|
|
10207
10306
|
}
|
|
10208
10307
|
const parent = dirname4(dir);
|
|
10209
10308
|
if (parent === dir) break;
|
|
@@ -10241,12 +10340,12 @@ function resolvePackagedBinary(platform = currentPlatform(), from = new URL("./p
|
|
|
10241
10340
|
}
|
|
10242
10341
|
const path = join3(dirname4(realpathSync(manifest)), "bin", "gws");
|
|
10243
10342
|
if (!existsSync2(path)) throw new Error(`GWS_UNAVAILABLE: ${name} has no bin/gws. Next step: Reinstall the plugin, or ${OVERRIDE}.`);
|
|
10244
|
-
const
|
|
10245
|
-
const mode =
|
|
10246
|
-
if (!
|
|
10343
|
+
const stat3 = lstatSync(path);
|
|
10344
|
+
const mode = stat3.mode & 4095;
|
|
10345
|
+
if (!stat3.isFile() || realpathSync(path) !== path || (mode & 320) !== 320 || (mode & ~493) !== 0) {
|
|
10247
10346
|
throw new Error(`BINARY_TRUST_FAILED: ${name} bin/gws must be a regular file, not a link, with owner read/execute and no permissions outside 0755`);
|
|
10248
10347
|
}
|
|
10249
|
-
const identity = `${
|
|
10348
|
+
const identity = `${stat3.size}:${stat3.mtimeMs}:${stat3.ino}`;
|
|
10250
10349
|
if (admitted.get(path) === identity) return path;
|
|
10251
10350
|
if (createHash2("sha256").update(readFileSync3(path)).digest("hex") !== GWS_TARGETS[target].sha256) {
|
|
10252
10351
|
throw new Error(`BINARY_TRUST_FAILED: ${name} bin/gws does not match the pinned ${target} release of gws 0.22.5`);
|
|
@@ -10432,6 +10531,62 @@ function runProcess(options) {
|
|
|
10432
10531
|
});
|
|
10433
10532
|
}
|
|
10434
10533
|
|
|
10534
|
+
// src/process/workdir.ts
|
|
10535
|
+
import { lstatSync as lstatSync2, mkdtempSync, readdirSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "node:fs";
|
|
10536
|
+
import { tmpdir } from "node:os";
|
|
10537
|
+
import { join as join6 } from "node:path";
|
|
10538
|
+
var PREFIX2 = "opencode-gws-";
|
|
10539
|
+
var OWNER = "owner.pid";
|
|
10540
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
10541
|
+
var UNOWNED_MS = 7 * DAY_MS;
|
|
10542
|
+
function createWorkDir(root = tmpdir()) {
|
|
10543
|
+
const dir = mkdtempSync(join6(root, PREFIX2));
|
|
10544
|
+
writeFileSync(join6(dir, ".env"), "");
|
|
10545
|
+
writeFileSync(join6(dir, OWNER), `${process.pid}
|
|
10546
|
+
`, { mode: 384 });
|
|
10547
|
+
return dir;
|
|
10548
|
+
}
|
|
10549
|
+
function alive(pid) {
|
|
10550
|
+
try {
|
|
10551
|
+
process.kill(pid, 0);
|
|
10552
|
+
return true;
|
|
10553
|
+
} catch (error2) {
|
|
10554
|
+
return error2.code === "EPERM";
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
function removeStaleWorkDirs(root = tmpdir(), now = Date.now()) {
|
|
10558
|
+
const removed = [];
|
|
10559
|
+
let names;
|
|
10560
|
+
try {
|
|
10561
|
+
names = readdirSync(root);
|
|
10562
|
+
} catch {
|
|
10563
|
+
return removed;
|
|
10564
|
+
}
|
|
10565
|
+
const uid = process.getuid?.();
|
|
10566
|
+
for (const name of names) {
|
|
10567
|
+
if (!/^opencode-gws-(?:probe-)?[A-Za-z0-9]{6}$/.test(name)) continue;
|
|
10568
|
+
const dir = join6(root, name);
|
|
10569
|
+
try {
|
|
10570
|
+
const stat3 = lstatSync2(dir);
|
|
10571
|
+
if (!stat3.isDirectory() || uid !== void 0 && stat3.uid !== uid) continue;
|
|
10572
|
+
const age = now - stat3.mtimeMs;
|
|
10573
|
+
if (age < DAY_MS) continue;
|
|
10574
|
+
let owner;
|
|
10575
|
+
try {
|
|
10576
|
+
owner = Number.parseInt(readFileSync4(join6(dir, OWNER), "utf8"), 10);
|
|
10577
|
+
} catch {
|
|
10578
|
+
owner = void 0;
|
|
10579
|
+
}
|
|
10580
|
+
const stale = owner !== void 0 && Number.isInteger(owner) && owner > 0 ? !alive(owner) : age >= UNOWNED_MS;
|
|
10581
|
+
if (!stale) continue;
|
|
10582
|
+
rmSync(dir, { recursive: true, force: true });
|
|
10583
|
+
removed.push(dir);
|
|
10584
|
+
} catch {
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
return removed;
|
|
10588
|
+
}
|
|
10589
|
+
|
|
10435
10590
|
// src/domain/schema.ts
|
|
10436
10591
|
function parseSchemaOutput(id, stdout, stderr) {
|
|
10437
10592
|
const start2 = stdout.indexOf("{");
|
|
@@ -10486,15 +10641,16 @@ var SchemaClient = class {
|
|
|
10486
10641
|
};
|
|
10487
10642
|
|
|
10488
10643
|
// src/runtime/accounts.ts
|
|
10489
|
-
import {
|
|
10644
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
10645
|
+
import { join as join7 } from "node:path";
|
|
10490
10646
|
|
|
10491
10647
|
// src/domain/digest.ts
|
|
10492
10648
|
function canonicalJson(value) {
|
|
10493
10649
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
10494
10650
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
10495
|
-
const
|
|
10496
|
-
const keys = Object.keys(
|
|
10497
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(
|
|
10651
|
+
const record3 = value;
|
|
10652
|
+
const keys = Object.keys(record3).sort();
|
|
10653
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record3[key])}`).join(",")}}`;
|
|
10498
10654
|
}
|
|
10499
10655
|
|
|
10500
10656
|
// src/auth/doctor.ts
|
|
@@ -10537,17 +10693,17 @@ var DISABLED = /accessNotConfigured|SERVICE_DISABLED|has not been used in projec
|
|
|
10537
10693
|
function probeOutcome(target, result) {
|
|
10538
10694
|
const parsed = parseResult(result, { maxInlineBytes: 64 * 1024 });
|
|
10539
10695
|
if (parsed.ok) return { ...target, status: "OK" };
|
|
10540
|
-
if (privilegeRefused(result, parsed)) {
|
|
10696
|
+
if (privilegeRefused(result, target.service, parsed)) {
|
|
10541
10697
|
return { ...target, status: "ADMIN_PRIVILEGE_REQUIRED", detail: "Google accepted the sign-in, but the account lacks the Workspace admin privilege this service needs" };
|
|
10542
10698
|
}
|
|
10543
|
-
const problem = authProblem(result, parsed);
|
|
10699
|
+
const problem = authProblem(result, target.service, parsed);
|
|
10544
10700
|
if (problem === "AUTH_REQUIRED") return { ...target, status: problem, detail: "Google rejected the sign-in" };
|
|
10545
10701
|
if (problem === "AUTH_SCOPE_MISSING") return { ...target, status: problem, detail: "Google says the sign-in lacks a scope for this service" };
|
|
10546
10702
|
const error2 = apiError(parsed.data);
|
|
10547
|
-
const
|
|
10703
|
+
const text4 = `${error2?.message ?? ""}
|
|
10548
10704
|
${result.stderr.toString("utf8")}`;
|
|
10549
|
-
if (DISABLED.test(
|
|
10550
|
-
const enableUrl = /https:\/\/console\.(?:developers|cloud)\.google\.com\/\S+/.exec(
|
|
10705
|
+
if (DISABLED.test(text4) || error2?.reasons.some((reason) => DISABLED.test(reason))) {
|
|
10706
|
+
const enableUrl = /https:\/\/console\.(?:developers|cloud)\.google\.com\/\S+/.exec(text4)?.[0]?.replace(/[.,)]+$/, "") ?? enableApisUrl([target.service]);
|
|
10551
10707
|
return { ...target, status: "API_DISABLED", detail: "the API is not enabled in the OAuth client's Google Cloud project", enableUrl };
|
|
10552
10708
|
}
|
|
10553
10709
|
if (PROBES[target.service]?.notFoundIsOk && (error2?.code === 404 || error2?.code === 400)) return { ...target, status: "OK" };
|
|
@@ -10641,9 +10797,8 @@ var Accounts = class {
|
|
|
10641
10797
|
);
|
|
10642
10798
|
return {
|
|
10643
10799
|
auditDegraded: this.runtime.audit.degraded,
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
...options.hostWarning ? { hostWarning: options.hostWarning } : {},
|
|
10800
|
+
version: options.pluginVersion,
|
|
10801
|
+
host: options.host ?? { version: null, range: HOST_RANGE, tested: false },
|
|
10647
10802
|
binary: { path: this.runtime.binary, version: this.runtime.version },
|
|
10648
10803
|
services: [...this.runtime.visibleServices()].sort(),
|
|
10649
10804
|
accounts
|
|
@@ -10668,9 +10823,15 @@ var Accounts = class {
|
|
|
10668
10823
|
this.runtime.assertActive(ctx.signal);
|
|
10669
10824
|
const env = buildChildEnv(profile, this.accountDir(account2), this.runtime.workDir, credential?.access);
|
|
10670
10825
|
report2(ctx, { status: "running", operation });
|
|
10671
|
-
const
|
|
10826
|
+
const scratch = options.cwd ? void 0 : await mkdtemp(join7(this.runtime.workDir, "call-"));
|
|
10827
|
+
let result;
|
|
10828
|
+
try {
|
|
10829
|
+
result = await this.spawn(argv, env, operation, ctx, options.paged === true, options.cwd ?? scratch);
|
|
10830
|
+
} finally {
|
|
10831
|
+
if (scratch) await rm(scratch, { recursive: true, force: true }).catch(() => void 0);
|
|
10832
|
+
}
|
|
10672
10833
|
if (credential) {
|
|
10673
|
-
const problem = authProblem(result);
|
|
10834
|
+
const problem = authProblem(result, serviceOf(operation));
|
|
10674
10835
|
if (problem) this.observeAuth(account2, { status: "needs_auth", code: problem, message: this.authMessage(account2, problem, options.scopes) });
|
|
10675
10836
|
else if (result.exitCode === 0 && options.dryRun !== true) this.observeAuth(account2, { status: "ok" });
|
|
10676
10837
|
}
|
|
@@ -10723,7 +10884,7 @@ var Accounts = class {
|
|
|
10723
10884
|
}
|
|
10724
10885
|
/** The plugin-owned per-account gws directory: its Discovery cache, and in credentials_file mode its token cache. */
|
|
10725
10886
|
accountDir(account2) {
|
|
10726
|
-
return ownedDir(
|
|
10887
|
+
return ownedDir(join7(this.runtime.paths.cacheDir, "accounts", this.integrationId(account2).replace(/^gws-/, "")));
|
|
10727
10888
|
}
|
|
10728
10889
|
/** Remediation text for an account's sign-in, consent and admin privilege problems, by credential mode. */
|
|
10729
10890
|
remediation(account2, scopes, service) {
|
|
@@ -10816,16 +10977,20 @@ function delegationField(profile) {
|
|
|
10816
10977
|
const services = delegationRequired(profile.enabledServices, profile.scopes);
|
|
10817
10978
|
return services.length > 0 ? { delegationRequired: services } : {};
|
|
10818
10979
|
}
|
|
10819
|
-
function
|
|
10820
|
-
return
|
|
10980
|
+
function serviceOf(operation) {
|
|
10981
|
+
return operation.slice(0, operation.indexOf("."));
|
|
10982
|
+
}
|
|
10983
|
+
function capitalize(text4) {
|
|
10984
|
+
return text4.charAt(0).toUpperCase() + text4.slice(1);
|
|
10821
10985
|
}
|
|
10822
10986
|
|
|
10823
10987
|
// src/domain/classify.ts
|
|
10824
|
-
var NOTIFYING_UPDATES = /* @__PURE__ */ new Set(["all", "externalonly"]);
|
|
10825
10988
|
function emailsAttendees(entry, request) {
|
|
10826
10989
|
if (entry.service !== "calendar" || entry.path.join(".") !== "events") return false;
|
|
10827
|
-
const
|
|
10828
|
-
|
|
10990
|
+
const updates = request.params?.["sendUpdates"];
|
|
10991
|
+
const legacy = request.params?.["sendNotifications"];
|
|
10992
|
+
const off = (value, none) => value === void 0 || value === false || typeof value === "string" && value.toLowerCase() === none;
|
|
10993
|
+
return !off(updates, "none") || !off(legacy, "false");
|
|
10829
10994
|
}
|
|
10830
10995
|
function effectiveCategory(entry, request) {
|
|
10831
10996
|
if (entry.category === "deny" || entry.schemaError) return "deny";
|
|
@@ -10841,27 +11006,393 @@ function isCommittable(category) {
|
|
|
10841
11006
|
|
|
10842
11007
|
// src/domain/command.ts
|
|
10843
11008
|
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
11009
|
+
|
|
11010
|
+
// src/domain/recipients.ts
|
|
11011
|
+
function addressesIn(text4) {
|
|
11012
|
+
const addresses = [];
|
|
11013
|
+
const unreadable = [];
|
|
11014
|
+
for (const match of text4.matchAll(/([A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]*)@(\[[^\]]*\]?|[^\s<>()[\]\\,;:"@]*)/g)) {
|
|
11015
|
+
const local = match[1].replace(/^\.+/, "");
|
|
11016
|
+
const domain = match[2].toLowerCase().replace(/\.+$/, "");
|
|
11017
|
+
if (!local && !domain) continue;
|
|
11018
|
+
if (!local || !/^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+$/u.test(domain)) {
|
|
11019
|
+
unreadable.push(match[0]);
|
|
11020
|
+
continue;
|
|
11021
|
+
}
|
|
11022
|
+
addresses.push(`${local.toLowerCase()}@${domain}`);
|
|
11023
|
+
}
|
|
11024
|
+
return { addresses, unreadable };
|
|
11025
|
+
}
|
|
11026
|
+
function domainOf(address) {
|
|
11027
|
+
return address.slice(address.lastIndexOf("@") + 1);
|
|
11028
|
+
}
|
|
11029
|
+
var Collector = class {
|
|
11030
|
+
addresses = /* @__PURE__ */ new Set();
|
|
11031
|
+
domains = /* @__PURE__ */ new Set();
|
|
11032
|
+
unknown;
|
|
11033
|
+
public;
|
|
11034
|
+
text(value) {
|
|
10847
11035
|
if (typeof value === "string") {
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
}
|
|
11036
|
+
const found = addressesIn(value);
|
|
11037
|
+
for (const address of found.addresses) this.addresses.add(address);
|
|
11038
|
+
if (found.unreadable.length > 0) this.fail(`an address could not be read: ${found.unreadable[0]}`);
|
|
11039
|
+
return;
|
|
11040
|
+
}
|
|
11041
|
+
if (Array.isArray(value)) for (const item of value) this.text(item);
|
|
11042
|
+
}
|
|
11043
|
+
domain(value) {
|
|
11044
|
+
if (typeof value === "string" && /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+$/u.test(value.trim())) this.domains.add(value.trim().toLowerCase());
|
|
11045
|
+
else this.fail("the domain it shares with could not be read");
|
|
11046
|
+
}
|
|
11047
|
+
add(set) {
|
|
11048
|
+
for (const address of set.addresses) this.addresses.add(address);
|
|
11049
|
+
for (const domain of set.domains) this.domains.add(domain);
|
|
11050
|
+
}
|
|
11051
|
+
fail(reason) {
|
|
11052
|
+
this.unknown ??= reason;
|
|
11053
|
+
}
|
|
11054
|
+
result() {
|
|
11055
|
+
return {
|
|
11056
|
+
addresses: [...this.addresses],
|
|
11057
|
+
domains: [...this.domains],
|
|
11058
|
+
...this.unknown ? { unknown: this.unknown } : {},
|
|
11059
|
+
...this.public ? { public: true } : {}
|
|
11060
|
+
};
|
|
11061
|
+
}
|
|
11062
|
+
};
|
|
11063
|
+
var RECIPIENT_KEYS = /* @__PURE__ */ new Set(["to", "cc", "bcc", "email", "emailaddress", "address", "recipient", "recipients", "attendees", "delegateemail", "forwardingemail"]);
|
|
11064
|
+
function scanRecipients(request) {
|
|
11065
|
+
const out = new Collector();
|
|
11066
|
+
const visit = (value, collecting) => {
|
|
11067
|
+
if (typeof value === "string") {
|
|
11068
|
+
if (collecting) out.text(value);
|
|
10851
11069
|
return;
|
|
10852
11070
|
}
|
|
10853
11071
|
if (Array.isArray(value)) {
|
|
10854
|
-
for (const item of value) visit(item,
|
|
11072
|
+
for (const item of value) visit(item, collecting);
|
|
10855
11073
|
return;
|
|
10856
11074
|
}
|
|
10857
11075
|
if (value && typeof value === "object") {
|
|
10858
|
-
for (const [
|
|
11076
|
+
for (const [key, child] of Object.entries(value)) visit(child, collecting || RECIPIENT_KEYS.has(key.toLowerCase()));
|
|
11077
|
+
}
|
|
11078
|
+
};
|
|
11079
|
+
visit(request.params, false);
|
|
11080
|
+
visit(request.body, false);
|
|
11081
|
+
visit(request.input, false);
|
|
11082
|
+
return out.result();
|
|
11083
|
+
}
|
|
11084
|
+
function record2(value) {
|
|
11085
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11086
|
+
}
|
|
11087
|
+
function text3(value) {
|
|
11088
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
11089
|
+
}
|
|
11090
|
+
function mimeRecipients(raw) {
|
|
11091
|
+
if (typeof raw !== "string" || raw.length === 0 || !/^[A-Za-z0-9+/_=\s-]+$/.test(raw)) return void 0;
|
|
11092
|
+
const message = Buffer.from(raw.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
11093
|
+
const end = /\r?\n\r?\n/.exec(message);
|
|
11094
|
+
const head = (end ? message.slice(0, end.index) : message).replace(/\r?\n[ \t]+/g, " ");
|
|
11095
|
+
const lines = head.split(/\r?\n/);
|
|
11096
|
+
if (!lines.every((line) => line === "" || /^[!-9;-~]+[ \t]*:/.test(line))) return void 0;
|
|
11097
|
+
return lines.filter((line) => /^(?:to|cc|bcc)[ \t]*:/i.test(line)).map((line) => line.slice(line.indexOf(":") + 1));
|
|
11098
|
+
}
|
|
11099
|
+
function headers(message) {
|
|
11100
|
+
const list = record2(record2(message)["payload"])["headers"];
|
|
11101
|
+
if (!Array.isArray(list)) return void 0;
|
|
11102
|
+
const out = /* @__PURE__ */ new Map();
|
|
11103
|
+
for (const header of list) {
|
|
11104
|
+
const name = text3(record2(header)["name"])?.toLowerCase();
|
|
11105
|
+
const value = record2(header)["value"];
|
|
11106
|
+
if (name && typeof value === "string") out.set(name, [...out.get(name) ?? [], value]);
|
|
11107
|
+
}
|
|
11108
|
+
return out;
|
|
11109
|
+
}
|
|
11110
|
+
function setOf(values) {
|
|
11111
|
+
const out = new Collector();
|
|
11112
|
+
out.text(values);
|
|
11113
|
+
return out.unknown ? void 0 : { addresses: [...out.addresses], domains: [] };
|
|
11114
|
+
}
|
|
11115
|
+
function plan(out, lookups = [], needsSelf = false) {
|
|
11116
|
+
return { known: out.result(), lookups, ...needsSelf ? { needsSelf: true } : {} };
|
|
11117
|
+
}
|
|
11118
|
+
function helperSend(fields) {
|
|
11119
|
+
return (request) => {
|
|
11120
|
+
const input = record2(request.input);
|
|
11121
|
+
const out = new Collector();
|
|
11122
|
+
for (const field of fields) out.text(input[field]);
|
|
11123
|
+
if (out.addresses.size === 0) out.fail("no recipient address was found");
|
|
11124
|
+
return plan(out);
|
|
11125
|
+
};
|
|
11126
|
+
}
|
|
11127
|
+
function replySpec(all) {
|
|
11128
|
+
return (request) => {
|
|
11129
|
+
const input = record2(request.input);
|
|
11130
|
+
const out = new Collector();
|
|
11131
|
+
for (const field of ["to", "cc", "bcc"]) out.text(input[field]);
|
|
11132
|
+
const removed = new Set(addressesIn([input["remove"]].flat().filter((item) => typeof item === "string").join(",")).addresses);
|
|
11133
|
+
const messageId = text3(input["messageId"]);
|
|
11134
|
+
if (!messageId) {
|
|
11135
|
+
out.fail("the message being replied to is not named");
|
|
11136
|
+
return plan(out);
|
|
11137
|
+
}
|
|
11138
|
+
const lookup = {
|
|
11139
|
+
operation: "gmail.users.messages.get",
|
|
11140
|
+
params: { userId: "me", id: messageId, format: "metadata", fields: "payload/headers" },
|
|
11141
|
+
what: "the message being replied to",
|
|
11142
|
+
extract(data, self) {
|
|
11143
|
+
const found = headers(data);
|
|
11144
|
+
if (!found) return void 0;
|
|
11145
|
+
const sender = found.get("reply-to") ?? found.get("from") ?? [];
|
|
11146
|
+
const senderSet = setOf(sender);
|
|
11147
|
+
if (!senderSet) return void 0;
|
|
11148
|
+
const values = [...sender];
|
|
11149
|
+
if (all || self !== void 0 && senderSet.addresses.includes(self)) values.push(...found.get("to") ?? []);
|
|
11150
|
+
if (all) values.push(...found.get("cc") ?? []);
|
|
11151
|
+
const set = setOf(values);
|
|
11152
|
+
if (!set) return void 0;
|
|
11153
|
+
const kept = all ? set.addresses.filter((address) => address !== self && !removed.has(address)) : set.addresses;
|
|
11154
|
+
return { addresses: kept, domains: [] };
|
|
11155
|
+
}
|
|
11156
|
+
};
|
|
11157
|
+
return plan(out, [lookup], true);
|
|
11158
|
+
};
|
|
11159
|
+
}
|
|
11160
|
+
function rawInto(out, raw) {
|
|
11161
|
+
const values = mimeRecipients(raw);
|
|
11162
|
+
if (values) out.text(values);
|
|
11163
|
+
else out.fail("the raw message has no readable header block");
|
|
11164
|
+
}
|
|
11165
|
+
var messagesSend = (request) => {
|
|
11166
|
+
const out = new Collector();
|
|
11167
|
+
if (request.upload) out.fail("the message is in an uploaded file");
|
|
11168
|
+
else if (record2(request.body)["raw"] === void 0) out.fail("the message has no raw field");
|
|
11169
|
+
else rawInto(out, record2(request.body)["raw"]);
|
|
11170
|
+
if (!out.unknown && out.addresses.size === 0) out.fail("the raw message has no To, Cc or Bcc header");
|
|
11171
|
+
return plan(out);
|
|
11172
|
+
};
|
|
11173
|
+
var draftsSend = (request) => {
|
|
11174
|
+
const out = new Collector();
|
|
11175
|
+
const body = record2(request.body);
|
|
11176
|
+
const raw = record2(body["message"])["raw"];
|
|
11177
|
+
if (raw !== void 0) rawInto(out, raw);
|
|
11178
|
+
const id = text3(body["id"]);
|
|
11179
|
+
if (!id) {
|
|
11180
|
+
out.fail("the draft is not named (body.id)");
|
|
11181
|
+
return plan(out);
|
|
11182
|
+
}
|
|
11183
|
+
const userId = text3(record2(request.params)["userId"]) ?? "me";
|
|
11184
|
+
const lookup = {
|
|
11185
|
+
operation: "gmail.users.drafts.get",
|
|
11186
|
+
params: { userId, id, format: "metadata", fields: "message/payload/headers" },
|
|
11187
|
+
what: "the draft",
|
|
11188
|
+
none: "the draft could not be read or names no recipient",
|
|
11189
|
+
extract(data) {
|
|
11190
|
+
const found = headers(record2(data)["message"]);
|
|
11191
|
+
return found ? setOf([...found.get("to") ?? [], ...found.get("cc") ?? [], ...found.get("bcc") ?? []]) : void 0;
|
|
10859
11192
|
}
|
|
10860
11193
|
};
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
11194
|
+
return plan(out, [lookup]);
|
|
11195
|
+
};
|
|
11196
|
+
function attendeesInto(out, body) {
|
|
11197
|
+
const attendees = record2(body)["attendees"];
|
|
11198
|
+
if (attendees === void 0) return;
|
|
11199
|
+
if (!Array.isArray(attendees)) {
|
|
11200
|
+
out.fail("the attendees could not be read");
|
|
11201
|
+
return;
|
|
11202
|
+
}
|
|
11203
|
+
for (const attendee of attendees) {
|
|
11204
|
+
const email2 = record2(attendee)["email"];
|
|
11205
|
+
if (typeof email2 === "string") out.text(email2);
|
|
11206
|
+
else out.fail("an attendee has no email");
|
|
11207
|
+
}
|
|
10864
11208
|
}
|
|
11209
|
+
var eventInsert = (request) => {
|
|
11210
|
+
const out = new Collector();
|
|
11211
|
+
attendeesInto(out, request.body);
|
|
11212
|
+
return plan(out);
|
|
11213
|
+
};
|
|
11214
|
+
var eventChange = (request, category) => {
|
|
11215
|
+
const out = new Collector();
|
|
11216
|
+
attendeesInto(out, request.body);
|
|
11217
|
+
if (category !== "send") return plan(out);
|
|
11218
|
+
const params = record2(request.params);
|
|
11219
|
+
const calendarId = text3(params["calendarId"]);
|
|
11220
|
+
const eventId = text3(params["eventId"]);
|
|
11221
|
+
if (!calendarId || !eventId) {
|
|
11222
|
+
out.fail("the event is not named (calendarId and eventId)");
|
|
11223
|
+
return plan(out);
|
|
11224
|
+
}
|
|
11225
|
+
const lookup = {
|
|
11226
|
+
operation: "calendar.events.get",
|
|
11227
|
+
params: { calendarId, eventId, fields: "attendees(email)" },
|
|
11228
|
+
what: "the event's guests",
|
|
11229
|
+
extract(data) {
|
|
11230
|
+
const attendees = record2(data)["attendees"] ?? [];
|
|
11231
|
+
if (!Array.isArray(attendees)) return void 0;
|
|
11232
|
+
const emails2 = attendees.map((attendee) => record2(attendee)["email"]);
|
|
11233
|
+
return emails2.every((email2) => typeof email2 === "string") ? setOf(emails2) : void 0;
|
|
11234
|
+
}
|
|
11235
|
+
};
|
|
11236
|
+
return plan(out, [lookup]);
|
|
11237
|
+
};
|
|
11238
|
+
var quickAdd = () => {
|
|
11239
|
+
const out = new Collector();
|
|
11240
|
+
out.fail("Google reads the guests from the text");
|
|
11241
|
+
return plan(out);
|
|
11242
|
+
};
|
|
11243
|
+
function drivePermissionInto(out, permission) {
|
|
11244
|
+
const type = text3(permission["type"]);
|
|
11245
|
+
if (type === "anyone") return;
|
|
11246
|
+
if (type === "domain") out.domain(permission["domain"]);
|
|
11247
|
+
else if (typeof permission["emailAddress"] === "string") out.text(permission["emailAddress"]);
|
|
11248
|
+
else out.fail("the permission names no emailAddress");
|
|
11249
|
+
}
|
|
11250
|
+
var drivePermissionCreate = (request) => {
|
|
11251
|
+
const out = new Collector();
|
|
11252
|
+
drivePermissionInto(out, record2(request.body));
|
|
11253
|
+
return plan(out);
|
|
11254
|
+
};
|
|
11255
|
+
var drivePermissionUpdate = (request) => {
|
|
11256
|
+
const out = new Collector();
|
|
11257
|
+
const params = record2(request.params);
|
|
11258
|
+
const fileId = text3(params["fileId"]);
|
|
11259
|
+
const permissionId = text3(params["permissionId"]);
|
|
11260
|
+
if (!fileId || !permissionId) {
|
|
11261
|
+
out.fail("the permission is not named (fileId and permissionId)");
|
|
11262
|
+
return plan(out);
|
|
11263
|
+
}
|
|
11264
|
+
const lookup = {
|
|
11265
|
+
operation: "drive.permissions.get",
|
|
11266
|
+
// The request's own parameters, so a shared-drive flag it carries applies to the read too.
|
|
11267
|
+
params: { ...params, fields: "type,emailAddress,domain" },
|
|
11268
|
+
what: "the permission",
|
|
11269
|
+
extract(data) {
|
|
11270
|
+
const found = new Collector();
|
|
11271
|
+
drivePermissionInto(found, record2(data));
|
|
11272
|
+
return found.unknown ? void 0 : found.result();
|
|
11273
|
+
}
|
|
11274
|
+
};
|
|
11275
|
+
return plan(out, [lookup]);
|
|
11276
|
+
};
|
|
11277
|
+
function aclScopeInto(out, type, value) {
|
|
11278
|
+
if (type === "default") out.public = true;
|
|
11279
|
+
else if (type === "domain") out.domain(value);
|
|
11280
|
+
else if ((type === "user" || type === "group") && typeof value === "string") out.text(value);
|
|
11281
|
+
else out.fail("the ACL scope could not be read");
|
|
11282
|
+
}
|
|
11283
|
+
var aclChange = (request) => {
|
|
11284
|
+
const out = new Collector();
|
|
11285
|
+
const scope = record2(record2(request.body)["scope"]);
|
|
11286
|
+
if (Object.keys(scope).length > 0) aclScopeInto(out, text3(scope["type"]), scope["value"]);
|
|
11287
|
+
const ruleId = text3(record2(request.params)["ruleId"]);
|
|
11288
|
+
if (ruleId) {
|
|
11289
|
+
const colon = ruleId.indexOf(":");
|
|
11290
|
+
aclScopeInto(out, colon < 0 ? ruleId : ruleId.slice(0, colon), colon < 0 ? void 0 : ruleId.slice(colon + 1));
|
|
11291
|
+
}
|
|
11292
|
+
if (Object.keys(scope).length === 0 && !ruleId) out.fail("the ACL scope could not be read");
|
|
11293
|
+
return plan(out);
|
|
11294
|
+
};
|
|
11295
|
+
function bodyAddress(field, optional2) {
|
|
11296
|
+
return (request) => {
|
|
11297
|
+
const out = new Collector();
|
|
11298
|
+
const body = record2(request.body);
|
|
11299
|
+
if (optional2?.(body)) return plan(out);
|
|
11300
|
+
const value = body[field];
|
|
11301
|
+
if (typeof value === "string" && value.includes("@")) out.text(value);
|
|
11302
|
+
else out.fail(`body.${field} is not an email address`);
|
|
11303
|
+
return plan(out);
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
var chatMember = (request) => {
|
|
11307
|
+
const out = new Collector();
|
|
11308
|
+
const name = text3(record2(record2(request.body)["member"])["name"]);
|
|
11309
|
+
if (name?.startsWith("users/") && name.includes("@")) out.text(name.slice("users/".length));
|
|
11310
|
+
else out.fail("the member is not named by email (users/<email>)");
|
|
11311
|
+
return plan(out);
|
|
11312
|
+
};
|
|
11313
|
+
var keepPermissions = (request) => {
|
|
11314
|
+
const out = new Collector();
|
|
11315
|
+
const requests = record2(request.body)["requests"];
|
|
11316
|
+
if (!Array.isArray(requests) || requests.length === 0) out.fail("the permission requests could not be read");
|
|
11317
|
+
else {
|
|
11318
|
+
for (const item of requests) {
|
|
11319
|
+
const email2 = record2(record2(item)["permission"])["email"];
|
|
11320
|
+
if (typeof email2 === "string") out.text(email2);
|
|
11321
|
+
else out.fail("a permission names no email");
|
|
11322
|
+
}
|
|
11323
|
+
}
|
|
11324
|
+
return plan(out);
|
|
11325
|
+
};
|
|
11326
|
+
var revoke = () => plan(new Collector());
|
|
11327
|
+
var SPECS = {
|
|
11328
|
+
"gmail.send": helperSend(["to", "cc", "bcc"]),
|
|
11329
|
+
"gmail.forward": helperSend(["to", "cc", "bcc"]),
|
|
11330
|
+
"gmail.reply": replySpec(false),
|
|
11331
|
+
"gmail.reply-all": replySpec(true),
|
|
11332
|
+
"gmail.users.messages.send": messagesSend,
|
|
11333
|
+
"gmail.users.drafts.send": draftsSend,
|
|
11334
|
+
"gmail.users.settings.delegates.create": bodyAddress("delegateEmail"),
|
|
11335
|
+
"gmail.users.settings.forwardingAddresses.create": bodyAddress("forwardingEmail"),
|
|
11336
|
+
"gmail.users.settings.updateAutoForwarding": bodyAddress("emailAddress", (body) => body["enabled"] === false),
|
|
11337
|
+
"gmail.users.settings.delegates.delete": revoke,
|
|
11338
|
+
"gmail.users.settings.forwardingAddresses.delete": revoke,
|
|
11339
|
+
"calendar.events.insert": eventInsert,
|
|
11340
|
+
"calendar.events.import": eventInsert,
|
|
11341
|
+
"calendar.events.quickAdd": quickAdd,
|
|
11342
|
+
"calendar.events.update": eventChange,
|
|
11343
|
+
"calendar.events.patch": eventChange,
|
|
11344
|
+
"calendar.events.delete": eventChange,
|
|
11345
|
+
"calendar.events.move": eventChange,
|
|
11346
|
+
"calendar.acl.insert": aclChange,
|
|
11347
|
+
"calendar.acl.update": aclChange,
|
|
11348
|
+
"calendar.acl.patch": aclChange,
|
|
11349
|
+
"calendar.acl.delete": revoke,
|
|
11350
|
+
"drive.permissions.create": drivePermissionCreate,
|
|
11351
|
+
"drive.permissions.update": drivePermissionUpdate,
|
|
11352
|
+
"drive.permissions.delete": revoke,
|
|
11353
|
+
"chat.spaces.members.create": chatMember,
|
|
11354
|
+
"chat.spaces.members.delete": revoke,
|
|
11355
|
+
"classroom.invitations.create": bodyAddress("userId"),
|
|
11356
|
+
"classroom.invitations.delete": revoke,
|
|
11357
|
+
"classroom.courses.studentGroups.studentGroupMembers.create": bodyAddress("userId"),
|
|
11358
|
+
"meet.spaces.members.create": bodyAddress("email"),
|
|
11359
|
+
"keep.notes.permissions.batchCreate": keepPermissions,
|
|
11360
|
+
"keep.notes.permissions.batchDelete": revoke
|
|
11361
|
+
};
|
|
11362
|
+
function recipientPlan(operation, category, request) {
|
|
11363
|
+
const spec2 = SPECS[operation];
|
|
11364
|
+
if (spec2) return spec2(request, category);
|
|
11365
|
+
const scanned = scanRecipients(request);
|
|
11366
|
+
if (category === "send" || category === "share") {
|
|
11367
|
+
return { known: { ...scanned, unknown: scanned.unknown ?? `the plugin cannot tell who ${operation} reaches` }, lookups: [] };
|
|
11368
|
+
}
|
|
11369
|
+
return { known: scanned, lookups: [] };
|
|
11370
|
+
}
|
|
11371
|
+
function withLookup(recipients, found, lookup) {
|
|
11372
|
+
if (!found) return { ...recipients, unknown: recipients.unknown ?? `${lookup.what} could not be read` };
|
|
11373
|
+
const merged = {
|
|
11374
|
+
...recipients,
|
|
11375
|
+
addresses: [.../* @__PURE__ */ new Set([...recipients.addresses, ...found.addresses])],
|
|
11376
|
+
domains: [.../* @__PURE__ */ new Set([...recipients.domains, ...found.domains])]
|
|
11377
|
+
};
|
|
11378
|
+
if (lookup.none && merged.addresses.length === 0 && merged.domains.length === 0) merged.unknown ??= lookup.none;
|
|
11379
|
+
return merged;
|
|
11380
|
+
}
|
|
11381
|
+
function recipientLabels(recipients) {
|
|
11382
|
+
return [...recipients.addresses, ...recipients.domains.map((domain) => `*@${domain}`)];
|
|
11383
|
+
}
|
|
11384
|
+
var ACCOUNT_ADDRESS_READS = {
|
|
11385
|
+
gmail: { operation: "gmail.users.getProfile", params: { userId: "me", fields: "emailAddress" }, extract: (data) => ownAddress(record2(data)["emailAddress"]) },
|
|
11386
|
+
drive: { operation: "drive.about.get", params: { fields: "user(emailAddress)" }, extract: (data) => ownAddress(record2(record2(data)["user"])["emailAddress"]) },
|
|
11387
|
+
calendar: { operation: "calendar.calendars.get", params: { calendarId: "primary", fields: "id" }, extract: (data) => ownAddress(record2(data)["id"]) }
|
|
11388
|
+
};
|
|
11389
|
+
function ownAddress(value) {
|
|
11390
|
+
if (typeof value !== "string") return void 0;
|
|
11391
|
+
const found = addressesIn(value);
|
|
11392
|
+
return found.addresses.length === 1 && found.unreadable.length === 0 ? found.addresses[0] : void 0;
|
|
11393
|
+
}
|
|
11394
|
+
|
|
11395
|
+
// src/domain/command.ts
|
|
10865
11396
|
function wantsPages(request) {
|
|
10866
11397
|
const pagination = request.pagination ?? {};
|
|
10867
11398
|
return pagination.all === true || pagination.maxPages !== void 0 || pagination.maxItems !== void 0;
|
|
@@ -10940,7 +11471,7 @@ function buildCommand(entry, request, config2, options) {
|
|
|
10940
11471
|
url: entry.pathTemplate,
|
|
10941
11472
|
params: redactParams(params),
|
|
10942
11473
|
bodySummary: request.body === void 0 ? void 0 : summarizeBody(request.body),
|
|
10943
|
-
recipients:
|
|
11474
|
+
recipients: recipientLabels(scanRecipients(request)),
|
|
10944
11475
|
uploads: request.upload ? [request.upload.path] : void 0,
|
|
10945
11476
|
destination: request.download?.path,
|
|
10946
11477
|
overwrite: request.download?.overwrite === true ? true : void 0,
|
|
@@ -10978,32 +11509,6 @@ function matches2(rule, category, account2, service, operation) {
|
|
|
10978
11509
|
function underRoot2(path, roots) {
|
|
10979
11510
|
return roots.some((root) => underRoot(path, root));
|
|
10980
11511
|
}
|
|
10981
|
-
function collectEmails(value, out) {
|
|
10982
|
-
if (typeof value === "string") {
|
|
10983
|
-
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) out.add(value.toLowerCase());
|
|
10984
|
-
return;
|
|
10985
|
-
}
|
|
10986
|
-
if (Array.isArray(value)) {
|
|
10987
|
-
for (const item of value) collectEmails(item, out);
|
|
10988
|
-
return;
|
|
10989
|
-
}
|
|
10990
|
-
if (value && typeof value === "object") {
|
|
10991
|
-
for (const [key, item] of Object.entries(value)) {
|
|
10992
|
-
if (key === "email" || key === "emailAddress" || key === "address" || key === "to" || key === "cc" || key === "bcc" || key === "recipient" || key === "attendees") {
|
|
10993
|
-
collectEmails(item, out);
|
|
10994
|
-
} else if (typeof item === "object") {
|
|
10995
|
-
collectEmails(item, out);
|
|
10996
|
-
}
|
|
10997
|
-
}
|
|
10998
|
-
}
|
|
10999
|
-
}
|
|
11000
|
-
function emails2(request) {
|
|
11001
|
-
const out = /* @__PURE__ */ new Set();
|
|
11002
|
-
collectEmails(request.params, out);
|
|
11003
|
-
collectEmails(request.body, out);
|
|
11004
|
-
collectEmails(request.input, out);
|
|
11005
|
-
return [...out];
|
|
11006
|
-
}
|
|
11007
11512
|
function helperPaths(request, keys) {
|
|
11008
11513
|
const input = request.input;
|
|
11009
11514
|
if (!input || typeof input !== "object") return [];
|
|
@@ -11021,13 +11526,19 @@ function publicSharing(request) {
|
|
|
11021
11526
|
const haystack = JSON.stringify({ params: request.params ?? {}, body: request.body ?? {}, input: request.input ?? {} }).toLowerCase();
|
|
11022
11527
|
return /"role"\s*:\s*"(anyone|public)"/.test(haystack) || /"type"\s*:\s*"(anyone|public)"/.test(haystack);
|
|
11023
11528
|
}
|
|
11024
|
-
function
|
|
11025
|
-
return
|
|
11529
|
+
function limitsRecipients(rule) {
|
|
11530
|
+
return rule.allowedRecipientDomains !== void 0 || rule.allowedRecipients !== void 0 || rule.deniedRecipientDomains !== void 0 || rule.allowExternalRecipients === false;
|
|
11531
|
+
}
|
|
11532
|
+
function matching(config2, entry, category, request) {
|
|
11533
|
+
return config2.policy.rules.filter((rule) => matches2(rule, category, request.account, entry.service, entry.id));
|
|
11534
|
+
}
|
|
11535
|
+
function needsAccountDomains(config2, entry, category, request) {
|
|
11536
|
+
return matching(config2, entry, category, request).some((rule) => rule.allowExternalRecipients === false);
|
|
11026
11537
|
}
|
|
11027
11538
|
function constrained(rule) {
|
|
11028
11539
|
return rule.allowedRecipientDomains !== void 0 || rule.allowedRecipients !== void 0 || rule.deniedRecipientDomains !== void 0 || rule.allowExternalRecipients !== void 0 || rule.allowPublicSharing !== void 0 || rule.allowedReadRoots !== void 0 || rule.allowedWriteRoots !== void 0 || rule.maxPages !== void 0 || rule.maxItems !== void 0;
|
|
11029
11540
|
}
|
|
11030
|
-
function evaluatePolicy(config2, entry, category, request) {
|
|
11541
|
+
function evaluatePolicy(config2, entry, category, request, facts) {
|
|
11031
11542
|
const constraints = {
|
|
11032
11543
|
maxPages: config2.execution.maxPages,
|
|
11033
11544
|
maxItems: config2.execution.maxItems,
|
|
@@ -11037,31 +11548,21 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11037
11548
|
writeRoots: config2.execution.allowedWriteRoots.length > 0 ? [config2.execution.allowedWriteRoots] : []
|
|
11038
11549
|
};
|
|
11039
11550
|
const violations = [];
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
if (!config2.accounts[request.account]) {
|
|
11044
|
-
return { effect: "deny", violations: ["account-unknown"], constraints };
|
|
11045
|
-
}
|
|
11551
|
+
const refuse = (violation) => ({ effect: "deny", violations: [violation], constraints, code: "POLICY_DENIED" });
|
|
11552
|
+
if (entry.category === "deny" || entry.schemaError) return refuse("manifest-deny");
|
|
11553
|
+
if (!config2.accounts[request.account]) return refuse("account-unknown");
|
|
11046
11554
|
const profile = config2.accounts[request.account];
|
|
11047
11555
|
const service = entry.service;
|
|
11048
|
-
if (!profile.enabledServices.includes(service))
|
|
11049
|
-
|
|
11050
|
-
}
|
|
11051
|
-
if (category === "deploy" || category === "watch" || category === "deny") {
|
|
11052
|
-
return { effect: "deny", violations: ["never-committable"], constraints };
|
|
11053
|
-
}
|
|
11556
|
+
if (!profile.enabledServices.includes(service)) return refuse("service-disabled");
|
|
11557
|
+
if (category === "deploy" || category === "watch" || category === "deny") return refuse("never-committable");
|
|
11054
11558
|
if (category === "admin") {
|
|
11055
|
-
if (!config2.admin.enabled)
|
|
11056
|
-
|
|
11057
|
-
}
|
|
11058
|
-
if (!config2.admin.allowedServices.includes(service)) {
|
|
11059
|
-
return { effect: "deny", violations: ["admin-service-not-allowed"], constraints };
|
|
11060
|
-
}
|
|
11559
|
+
if (!config2.admin.enabled) return refuse("admin-disabled");
|
|
11560
|
+
if (!config2.admin.allowedServices.includes(service)) return refuse("admin-service-not-allowed");
|
|
11061
11561
|
}
|
|
11062
11562
|
let denied2 = false;
|
|
11063
11563
|
let ruleId;
|
|
11064
|
-
|
|
11564
|
+
let unknownRecipients = 0;
|
|
11565
|
+
let unknownDomain = 0;
|
|
11065
11566
|
const readPaths = [
|
|
11066
11567
|
...request.upload?.path ? [request.upload.path] : [],
|
|
11067
11568
|
...helperPaths(request, ["path", "file", "filePath", "source", "attachments"])
|
|
@@ -11070,28 +11571,44 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11070
11571
|
...request.download?.path ? [request.download.path] : [],
|
|
11071
11572
|
...helperPaths(request, ["destination", "output", "outPath"])
|
|
11072
11573
|
];
|
|
11073
|
-
const
|
|
11074
|
-
const
|
|
11075
|
-
|
|
11076
|
-
|
|
11574
|
+
const recipients = facts === "pending" ? void 0 : facts.recipients;
|
|
11575
|
+
const targets = recipients ? [
|
|
11576
|
+
...recipients.addresses.map((address) => ({ label: address, address, domain: domainOf(address) })),
|
|
11577
|
+
...recipients.domains.map((domain) => ({ label: `*@${domain}`, address: void 0, domain }))
|
|
11578
|
+
] : [];
|
|
11579
|
+
const own2 = facts === "pending" || !facts.accountDomains ? void 0 : new Set(facts.accountDomains.map((value) => value.toLowerCase()));
|
|
11580
|
+
const isPublic = publicSharing(request) || recipients?.public === true;
|
|
11581
|
+
for (const rule of matching(config2, entry, category, request)) {
|
|
11077
11582
|
const before = violations.length;
|
|
11078
|
-
if (
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11583
|
+
if (recipients && limitsRecipients(rule)) {
|
|
11584
|
+
if (recipients.unknown) {
|
|
11585
|
+
violations.push(`rule ${rule.id} limits recipients, but they could not be determined: ${recipients.unknown}`);
|
|
11586
|
+
unknownRecipients++;
|
|
11587
|
+
}
|
|
11588
|
+
if (rule.allowedRecipientDomains) {
|
|
11589
|
+
const allowed = new Set(rule.allowedRecipientDomains.map((value) => value.toLowerCase()));
|
|
11590
|
+
const bad = targets.filter((target) => !allowed.has(target.domain));
|
|
11591
|
+
if (bad.length > 0) violations.push(`recipients outside allowed domains: ${bad.map((target) => target.label).join(", ")}`);
|
|
11592
|
+
}
|
|
11593
|
+
if (rule.allowedRecipients) {
|
|
11594
|
+
const allowed = new Set(rule.allowedRecipients.map((value) => value.trim().toLowerCase()));
|
|
11595
|
+
const bad = targets.filter((target) => target.address === void 0 || !allowed.has(target.address));
|
|
11596
|
+
if (bad.length > 0) violations.push(`recipients not in allowedRecipients: ${bad.map((target) => target.label).join(", ")}`);
|
|
11597
|
+
}
|
|
11598
|
+
if (rule.deniedRecipientDomains) {
|
|
11599
|
+
const deniedDomains = new Set(rule.deniedRecipientDomains.map((value) => value.toLowerCase()));
|
|
11600
|
+
const bad = targets.filter((target) => deniedDomains.has(target.domain));
|
|
11601
|
+
if (bad.length > 0) violations.push(`recipients in denied domains: ${bad.map((target) => target.label).join(", ")}`);
|
|
11602
|
+
}
|
|
11603
|
+
if (rule.allowExternalRecipients === false && (targets.length > 0 || recipients.unknown)) {
|
|
11604
|
+
if (!own2) {
|
|
11605
|
+
violations.push(`rule ${rule.id} disables external recipients, but the account's own domain is unknown`);
|
|
11606
|
+
unknownDomain++;
|
|
11607
|
+
} else {
|
|
11608
|
+
const external = targets.filter((target) => !own2.has(target.domain));
|
|
11609
|
+
if (external.length > 0) violations.push(`external recipients are disabled: ${external.map((target) => target.label).join(", ")}`);
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11095
11612
|
}
|
|
11096
11613
|
if (rule.allowPublicSharing === false && isPublic) {
|
|
11097
11614
|
violations.push("public sharing is disabled");
|
|
@@ -11129,16 +11646,18 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11129
11646
|
violations.push("admin plans require a non-empty reason");
|
|
11130
11647
|
}
|
|
11131
11648
|
if (violations.length > 0 || denied2) {
|
|
11132
|
-
|
|
11649
|
+
const definite = violations.length - unknownRecipients - unknownDomain;
|
|
11650
|
+
const code2 = definite > 0 || violations.length === 0 ? "POLICY_DENIED" : unknownRecipients > 0 ? "RECIPIENTS_UNKNOWN" : "ACCOUNT_DOMAIN_UNKNOWN";
|
|
11651
|
+
return { effect: "deny", ruleId, violations: violations.length > 0 ? violations : [`denied by rule ${ruleId ?? "policy"}`], constraints, code: code2 };
|
|
11133
11652
|
}
|
|
11134
|
-
return { effect: "allow", ruleId, violations, constraints };
|
|
11653
|
+
return { effect: "allow", ruleId, violations, constraints, code: "POLICY_DENIED" };
|
|
11135
11654
|
}
|
|
11136
11655
|
|
|
11137
11656
|
// src/process/staging.ts
|
|
11138
11657
|
import { createHash as createHash3 } from "node:crypto";
|
|
11139
11658
|
import { constants } from "node:fs";
|
|
11140
|
-
import { link, lstat, mkdir as mkdir2, mkdtemp, open as open2, realpath, rename, rm, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
11141
|
-
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute7, join as
|
|
11659
|
+
import { link, lstat, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, realpath, rename as rename2, rm as rm2, stat as stat2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
11660
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute7, join as join8 } from "node:path";
|
|
11142
11661
|
var CHUNK = 64 * 1024;
|
|
11143
11662
|
var READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
11144
11663
|
var Stage = class _Stage {
|
|
@@ -11150,13 +11669,13 @@ var Stage = class _Stage {
|
|
|
11150
11669
|
signal;
|
|
11151
11670
|
static async create(workDir, signal) {
|
|
11152
11671
|
throwIfCancelled(signal);
|
|
11153
|
-
const dir = await
|
|
11672
|
+
const dir = await mkdtemp2(join8(workDir, "call-"));
|
|
11154
11673
|
try {
|
|
11155
11674
|
throwIfCancelled(signal);
|
|
11156
|
-
await writeFile2(
|
|
11675
|
+
await writeFile2(join8(dir, ".env"), "", { mode: 384 });
|
|
11157
11676
|
return new _Stage(dir, signal);
|
|
11158
11677
|
} catch (error2) {
|
|
11159
|
-
await
|
|
11678
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
11160
11679
|
throw error2;
|
|
11161
11680
|
}
|
|
11162
11681
|
}
|
|
@@ -11170,7 +11689,7 @@ var Stage = class _Stage {
|
|
|
11170
11689
|
/** Create the directory gws writes the output into. */
|
|
11171
11690
|
async prepareOutput(output) {
|
|
11172
11691
|
throwIfCancelled(this.signal);
|
|
11173
|
-
await mkdir2(dirname5(
|
|
11692
|
+
await mkdir2(dirname5(join8(this.dir, output.rel)), { recursive: true, mode: 448 });
|
|
11174
11693
|
}
|
|
11175
11694
|
/**
|
|
11176
11695
|
* Move the staged output to its destination: size cap, then the same
|
|
@@ -11180,7 +11699,7 @@ var Stage = class _Stage {
|
|
|
11180
11699
|
*/
|
|
11181
11700
|
async deliver(output, limits) {
|
|
11182
11701
|
throwIfCancelled(this.signal);
|
|
11183
|
-
const staged =
|
|
11702
|
+
const staged = join8(this.dir, output.rel);
|
|
11184
11703
|
const info = await lstat(staged).catch(() => void 0);
|
|
11185
11704
|
if (!info?.isFile()) throw new GwsError("DOWNLOAD_INVALID", "gws did not write the output file", { next: "Report it; the plan is used up." });
|
|
11186
11705
|
if (info.size > limits.maxDownloadBytes) {
|
|
@@ -11192,7 +11711,7 @@ var Stage = class _Stage {
|
|
|
11192
11711
|
return { path: output.destination, bytes: info.size, sha256, ...cleanupDegraded ? { cleanupDegraded: true } : {} };
|
|
11193
11712
|
}
|
|
11194
11713
|
async dispose() {
|
|
11195
|
-
await
|
|
11714
|
+
await rm2(this.dir, { recursive: true, force: true });
|
|
11196
11715
|
}
|
|
11197
11716
|
async stageInput(input, maxBytes, roots) {
|
|
11198
11717
|
throwIfCancelled(this.signal);
|
|
@@ -11204,7 +11723,7 @@ var Stage = class _Stage {
|
|
|
11204
11723
|
next: "Use a file inside execution.allowedReadRoots, or ask the user to add its folder there."
|
|
11205
11724
|
});
|
|
11206
11725
|
}
|
|
11207
|
-
if (!(await
|
|
11726
|
+
if (!(await stat2(real)).isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11208
11727
|
let source;
|
|
11209
11728
|
let target;
|
|
11210
11729
|
try {
|
|
@@ -11212,7 +11731,7 @@ var Stage = class _Stage {
|
|
|
11212
11731
|
const info = await source.stat();
|
|
11213
11732
|
if (!info.isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11214
11733
|
if (info.size > maxBytes) throw uploadLimit(input.source, info.size, maxBytes);
|
|
11215
|
-
const destination =
|
|
11734
|
+
const destination = join8(this.dir, input.rel);
|
|
11216
11735
|
throwIfCancelled(this.signal);
|
|
11217
11736
|
await mkdir2(dirname5(destination), { recursive: true, mode: 448 });
|
|
11218
11737
|
throwIfCancelled(this.signal);
|
|
@@ -11242,7 +11761,7 @@ async function checkDestination(output, limits) {
|
|
|
11242
11761
|
if (!isAbsolute7(output.destination)) throw new GwsError("DOWNLOAD_INVALID", `'${output.destination}' must be an absolute path`);
|
|
11243
11762
|
const parent = await realpath(dirname5(output.destination)).catch(() => void 0);
|
|
11244
11763
|
if (!parent) throw new GwsError("DOWNLOAD_INVALID", `the directory for '${output.destination}' does not exist`);
|
|
11245
|
-
const target =
|
|
11764
|
+
const target = join8(parent, basename2(output.destination));
|
|
11246
11765
|
if (!withinRootSets(target, await realRootSets(limits.writeRoots))) {
|
|
11247
11766
|
throw new GwsError("POLICY_DENIED", `download path '${output.destination}' resolves outside allowed write roots`, {
|
|
11248
11767
|
next: "Use a destination inside execution.allowedWriteRoots, or ask the user to add its folder there."
|
|
@@ -11256,20 +11775,20 @@ async function checkDestination(output, limits) {
|
|
|
11256
11775
|
async function place(staged, target, output, signal) {
|
|
11257
11776
|
throwIfCancelled(signal);
|
|
11258
11777
|
try {
|
|
11259
|
-
if (output.overwrite) await
|
|
11778
|
+
if (output.overwrite) await rename2(staged, target);
|
|
11260
11779
|
else await link(staged, target);
|
|
11261
11780
|
return false;
|
|
11262
11781
|
} catch (error2) {
|
|
11263
11782
|
if (code(error2) === "EEXIST") throw exists(output.destination);
|
|
11264
11783
|
if (code(error2) !== "EXDEV" && code(error2) !== "EPERM" && code(error2) !== "ENOTSUP") throw error2;
|
|
11265
11784
|
}
|
|
11266
|
-
const temp =
|
|
11785
|
+
const temp = join8(dirname5(target), `.${basename2(target)}.${process.pid}.${Date.now()}.partial`);
|
|
11267
11786
|
let cleanupDegraded = false;
|
|
11268
11787
|
try {
|
|
11269
11788
|
throwIfCancelled(signal);
|
|
11270
11789
|
await copyDurable(staged, temp, signal);
|
|
11271
11790
|
throwIfCancelled(signal);
|
|
11272
|
-
if (output.overwrite) await
|
|
11791
|
+
if (output.overwrite) await rename2(temp, target);
|
|
11273
11792
|
else {
|
|
11274
11793
|
try {
|
|
11275
11794
|
await link(temp, target);
|
|
@@ -11338,6 +11857,17 @@ function code(error2) {
|
|
|
11338
11857
|
// src/runtime/operations.ts
|
|
11339
11858
|
var METHOD_FIELDS = ["params", "body", "fields", "pagination", "upload", "download"];
|
|
11340
11859
|
var PLAN_INSTEAD = "Call gws_plan with the same request, then commit with the tool named by the plan's category.";
|
|
11860
|
+
var SAVE_INSTEAD = "Google answered with a body that is not JSON. To save it, plan the same request with download.path, then commit it with gws_export.";
|
|
11861
|
+
var ADDRESS_TTL_MS = 10 * 60 * 1e3;
|
|
11862
|
+
function policyError(decision, account2) {
|
|
11863
|
+
const detail = decision.violations.join("; ");
|
|
11864
|
+
if (decision.code === "ACCOUNT_DOMAIN_UNKNOWN") {
|
|
11865
|
+
return new GwsError("ACCOUNT_DOMAIN_UNKNOWN", detail, {
|
|
11866
|
+
next: `Ask the user to set accounts.${account2}.domains to the account's email domains, or to enable Gmail, Drive or Calendar for '${account2}' so the plugin can read its address.`
|
|
11867
|
+
});
|
|
11868
|
+
}
|
|
11869
|
+
return new GwsError(decision.code, detail);
|
|
11870
|
+
}
|
|
11341
11871
|
var Operations = class {
|
|
11342
11872
|
constructor(runtime, accounts) {
|
|
11343
11873
|
this.runtime = runtime;
|
|
@@ -11345,6 +11875,8 @@ var Operations = class {
|
|
|
11345
11875
|
}
|
|
11346
11876
|
runtime;
|
|
11347
11877
|
accounts;
|
|
11878
|
+
/** Each account's own address, once read. */
|
|
11879
|
+
addresses = /* @__PURE__ */ new Map();
|
|
11348
11880
|
/** The configured account a call names, or the only one when it names none. */
|
|
11349
11881
|
account(alias) {
|
|
11350
11882
|
const aliases = Object.keys(this.runtime.config.accounts);
|
|
@@ -11416,8 +11948,9 @@ var Operations = class {
|
|
|
11416
11948
|
throw new GwsError("POLICY_DENIED", `'${entry.id}' is ${category2}; use gws_plan and the matching commit tool`, { next: PLAN_INSTEAD });
|
|
11417
11949
|
}
|
|
11418
11950
|
}
|
|
11419
|
-
const
|
|
11420
|
-
|
|
11951
|
+
const facts = { recipients: recipientPlan(entry.id, "read", request).known, accountDomains: this.runtime.config.accounts[request.account]?.domains };
|
|
11952
|
+
const decision2 = evaluatePolicy(this.runtime.config, entry, "read", request, facts);
|
|
11953
|
+
if (decision2.effect === "deny") throw policyError(decision2, request.account);
|
|
11421
11954
|
return { category: "read", decision: decision2 };
|
|
11422
11955
|
}
|
|
11423
11956
|
const category = helper ? helper.category : effectiveCategory(entry, request);
|
|
@@ -11426,8 +11959,8 @@ var Operations = class {
|
|
|
11426
11959
|
next: category === "read" ? "Run it with gws_read." : "Tell the user the plugin never runs it."
|
|
11427
11960
|
});
|
|
11428
11961
|
}
|
|
11429
|
-
const decision = evaluatePolicy(this.runtime.config, entry, category, request);
|
|
11430
|
-
if (decision.effect === "deny") throw
|
|
11962
|
+
const decision = evaluatePolicy(this.runtime.config, entry, category, request, "pending");
|
|
11963
|
+
if (decision.effect === "deny") throw policyError(decision, request.account);
|
|
11431
11964
|
if (category === "admin" && (!this.runtime.config.admin.enabled || decision.effect !== "allow")) {
|
|
11432
11965
|
throw new GwsError("POLICY_DENIED", "admin operations require admin.enabled, an allowed service, and a reason when one is required");
|
|
11433
11966
|
}
|
|
@@ -11440,11 +11973,13 @@ var Operations = class {
|
|
|
11440
11973
|
inspect(input, mode) {
|
|
11441
11974
|
const resolved = this.resolve(input);
|
|
11442
11975
|
const { category } = this.gate(resolved, mode);
|
|
11976
|
+
const reads = mode === "plan" ? recipientPlan(resolved.entry.id, category, resolved.request).lookups.map((lookup) => lookup.operation) : [];
|
|
11443
11977
|
return {
|
|
11444
11978
|
account: resolved.request.account,
|
|
11445
11979
|
operation: resolved.entry.id,
|
|
11446
11980
|
service: resolved.entry.service,
|
|
11447
|
-
category
|
|
11981
|
+
category,
|
|
11982
|
+
...reads.length > 0 ? { reads } : {}
|
|
11448
11983
|
};
|
|
11449
11984
|
}
|
|
11450
11985
|
/**
|
|
@@ -11500,7 +12035,7 @@ var Operations = class {
|
|
|
11500
12035
|
requestedInlineBytes: helper ? void 0 : request.response?.maxInlineBytes
|
|
11501
12036
|
});
|
|
11502
12037
|
const failed = helper ? result.exitCode !== 0 || isApiError(parsed.data) : !parsed.ok;
|
|
11503
|
-
if (failed) throw processError(result, parsed, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
12038
|
+
if (failed) throw processError(result, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11504
12039
|
const pages = paged ? capPageItems(parsed.data, itemCap(request, limits)) : void 0;
|
|
11505
12040
|
const cap = inlineCap(this.runtime.config.execution.maxInlineBytes, request.response?.mode, request.response?.maxInlineBytes);
|
|
11506
12041
|
const truncated = pages ? Buffer.byteLength(JSON.stringify(pages.data)) > cap : parsed.truncated;
|
|
@@ -11517,7 +12052,7 @@ var Operations = class {
|
|
|
11517
12052
|
ok: true,
|
|
11518
12053
|
operation: entry.id,
|
|
11519
12054
|
category: "read",
|
|
11520
|
-
data: helper ? parsed.data ?? parsed.raw ?? result.stdout.toString("utf8") : pages ? pages.data : parsed.data,
|
|
12055
|
+
data: helper ? parsed.data ?? parsed.raw ?? result.stdout.toString("utf8") : pages ? pages.data : withoutSavedBody(parsed.data, SAVE_INSTEAD),
|
|
11521
12056
|
truncated,
|
|
11522
12057
|
diagnostics: parsed.diagnostics,
|
|
11523
12058
|
pagination: pages ? { pages: pages.pages, items: pages.items, capped: pages.capped } : void 0,
|
|
@@ -11532,8 +12067,12 @@ var Operations = class {
|
|
|
11532
12067
|
const resolved = this.resolve(input);
|
|
11533
12068
|
const { entry, helper, request } = resolved;
|
|
11534
12069
|
const started = Date.now();
|
|
11535
|
-
const { category
|
|
12070
|
+
const { category } = this.gate(resolved, "plan");
|
|
11536
12071
|
const commitCategory = category;
|
|
12072
|
+
const recipientsPlan = recipientPlan(entry.id, category, request);
|
|
12073
|
+
const facts = await this.policyFacts(resolved, category, recipientsPlan, ctx);
|
|
12074
|
+
const decision = evaluatePolicy(this.runtime.config, entry, category, request, facts);
|
|
12075
|
+
if (decision.effect === "deny") throw policyError(decision, request.account);
|
|
11537
12076
|
const limits = fileLimits(decision);
|
|
11538
12077
|
let dryRunArgv;
|
|
11539
12078
|
let commitArgv;
|
|
@@ -11564,14 +12103,16 @@ var Operations = class {
|
|
|
11564
12103
|
const dry = await this.accounts.run(dryRunArgv, request.account, entry.id, ctx, { scopes: entry.scopes, dryRun: true, cwd: stage?.dir });
|
|
11565
12104
|
report2(ctx, { status: "parsing", operation: entry.id });
|
|
11566
12105
|
parsed = parseResult(dry, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
11567
|
-
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
12106
|
+
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11568
12107
|
} finally {
|
|
11569
12108
|
await stage?.dispose();
|
|
11570
12109
|
}
|
|
11571
12110
|
throwIfCancelled(ctx.signal);
|
|
11572
12111
|
const reason = request.reason?.trim();
|
|
11573
|
-
const
|
|
11574
|
-
const
|
|
12112
|
+
const { recipients } = facts;
|
|
12113
|
+
const reached = category === "send" || category === "share" ? { ...preview, recipients: recipientLabels(recipients), ...recipients.unknown ? { recipientsUnknown: recipients.unknown } : {} } : preview;
|
|
12114
|
+
const planPreview = reason ? { ...reached, reason } : reached;
|
|
12115
|
+
const plan2 = this.runtime.plans.create({
|
|
11575
12116
|
account: request.account,
|
|
11576
12117
|
sessionId: ctx.sessionId,
|
|
11577
12118
|
agentId: ctx.agentId,
|
|
@@ -11581,6 +12122,8 @@ var Operations = class {
|
|
|
11581
12122
|
argv: commitArgv,
|
|
11582
12123
|
preview: planPreview,
|
|
11583
12124
|
files: planned,
|
|
12125
|
+
// Recipients read at plan time can change before the commit (a draft, an event's guests): the commit reads them again.
|
|
12126
|
+
...recipientsPlan.lookups.length > 0 ? { recheck: { request, recipients: facts.recipients.addresses.concat(facts.recipients.domains) } } : {},
|
|
11584
12127
|
ttlSeconds: this.runtime.config.policy.planTtlSeconds
|
|
11585
12128
|
});
|
|
11586
12129
|
this.runtime.audit.emit({
|
|
@@ -11594,8 +12137,8 @@ var Operations = class {
|
|
|
11594
12137
|
});
|
|
11595
12138
|
return {
|
|
11596
12139
|
ok: true,
|
|
11597
|
-
planId:
|
|
11598
|
-
expiresAt: new Date(
|
|
12140
|
+
planId: plan2.id,
|
|
12141
|
+
expiresAt: new Date(plan2.expiresAt).toISOString(),
|
|
11599
12142
|
category: commitCategory,
|
|
11600
12143
|
preview: planPreview,
|
|
11601
12144
|
dryRun: redactDryRun(parsed.data)
|
|
@@ -11606,7 +12149,7 @@ var Operations = class {
|
|
|
11606
12149
|
throwIfCancelled(ctx.signal);
|
|
11607
12150
|
if (existing.files?.output) await checkDestination(existing.files.output, existing.files.limits);
|
|
11608
12151
|
this.runtime.assertActive(ctx.signal);
|
|
11609
|
-
const
|
|
12152
|
+
const plan2 = this.runtime.plans.acquire(input.planId, {
|
|
11610
12153
|
sessionId: ctx.sessionId,
|
|
11611
12154
|
agentId: ctx.agentId,
|
|
11612
12155
|
account: existing.account,
|
|
@@ -11616,39 +12159,41 @@ var Operations = class {
|
|
|
11616
12159
|
let stage;
|
|
11617
12160
|
let receipt;
|
|
11618
12161
|
try {
|
|
11619
|
-
if (
|
|
11620
|
-
if (stage
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
12162
|
+
if (plan2.recheck) await this.recheck(plan2, ctx);
|
|
12163
|
+
if (plan2.files) stage = await Stage.create(this.runtime.workDir, ctx.signal);
|
|
12164
|
+
if (stage && plan2.files) {
|
|
12165
|
+
const staged = await stage.stageInputs(plan2.files.inputs, plan2.files.limits);
|
|
12166
|
+
refuseChanged(plan2.files.inputs, staged);
|
|
12167
|
+
if (plan2.files.output) await stage.prepareOutput(plan2.files.output);
|
|
12168
|
+
}
|
|
12169
|
+
const method = commandIndex().methods.get(plan2.operation);
|
|
11626
12170
|
const scopes = method?.scopes;
|
|
11627
|
-
const result = await this.accounts.run(
|
|
11628
|
-
report2(ctx, { status: "parsing", operation:
|
|
12171
|
+
const result = await this.accounts.run(plan2.argv, plan2.account, plan2.operation, ctx, { scopes, cwd: stage?.dir });
|
|
12172
|
+
report2(ctx, { status: "parsing", operation: plan2.operation });
|
|
11629
12173
|
const parsed = parseResult(result, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
11630
|
-
if (!parsed.ok) throw processError(result, parsed, this.accounts.remediation(
|
|
11631
|
-
const delivered = stage &&
|
|
12174
|
+
if (!parsed.ok) throw processError(result, parsed, plan2.service, this.accounts.remediation(plan2.account, scopes, plan2.service));
|
|
12175
|
+
const delivered = stage && plan2.files?.output ? await stage.deliver(plan2.files.output, plan2.files.limits) : void 0;
|
|
11632
12176
|
const file = delivered ? { path: delivered.path, bytes: delivered.bytes, sha256: delivered.sha256 } : void 0;
|
|
11633
|
-
this.runtime.plans.complete(
|
|
12177
|
+
this.runtime.plans.complete(plan2.id);
|
|
11634
12178
|
this.runtime.audit.emit({
|
|
11635
12179
|
event: "operation.commit",
|
|
11636
12180
|
outcome: "success",
|
|
11637
|
-
account:
|
|
12181
|
+
account: plan2.account,
|
|
11638
12182
|
category,
|
|
11639
|
-
operation:
|
|
12183
|
+
operation: plan2.operation,
|
|
11640
12184
|
durationMs: Date.now() - started
|
|
11641
12185
|
});
|
|
11642
|
-
|
|
12186
|
+
const answer = plan2.files?.output ? parsed.data : withoutSavedBody(parsed.data);
|
|
12187
|
+
receipt = { ok: true, planId: plan2.id, category, operation: plan2.operation, result: answer ?? parsed.raw, file, ...delivered?.cleanupDegraded ? { cleanupDegraded: true } : {} };
|
|
11643
12188
|
return receipt;
|
|
11644
12189
|
} catch (error2) {
|
|
11645
|
-
this.runtime.plans.release(
|
|
12190
|
+
this.runtime.plans.release(plan2.id);
|
|
11646
12191
|
this.runtime.audit.emit({
|
|
11647
12192
|
event: "operation.commit",
|
|
11648
12193
|
outcome: "error",
|
|
11649
|
-
account:
|
|
12194
|
+
account: plan2.account,
|
|
11650
12195
|
category,
|
|
11651
|
-
operation:
|
|
12196
|
+
operation: plan2.operation,
|
|
11652
12197
|
durationMs: Date.now() - started,
|
|
11653
12198
|
errorCode: auditErrorCode(error2)
|
|
11654
12199
|
});
|
|
@@ -11668,6 +12213,85 @@ var Operations = class {
|
|
|
11668
12213
|
}
|
|
11669
12214
|
}
|
|
11670
12215
|
}
|
|
12216
|
+
/**
|
|
12217
|
+
* The recipient facts for a plan: what the request carries, the reads
|
|
12218
|
+
* that find the rest, and the account's own domains when a rule needs them.
|
|
12219
|
+
* A read that fails leaves the recipients unknown, which a recipient rule refuses.
|
|
12220
|
+
*/
|
|
12221
|
+
async policyFacts(resolved, category, plan2, ctx) {
|
|
12222
|
+
const { entry, request } = resolved;
|
|
12223
|
+
let recipients = plan2.known;
|
|
12224
|
+
if (plan2.lookups.length > 0) {
|
|
12225
|
+
const self = plan2.needsSelf ? await this.ownAddress(request.account, entry.service, ctx) : void 0;
|
|
12226
|
+
for (const lookup of plan2.lookups) recipients = withLookup(recipients, await this.lookup(lookup, request.account, ctx, self), lookup);
|
|
12227
|
+
}
|
|
12228
|
+
const configured = this.runtime.config.accounts[request.account]?.domains;
|
|
12229
|
+
const reaches = recipients.addresses.length > 0 || recipients.domains.length > 0 || recipients.unknown !== void 0;
|
|
12230
|
+
if (configured || !reaches || !needsAccountDomains(this.runtime.config, entry, category, request)) return { recipients, accountDomains: configured };
|
|
12231
|
+
const address = await this.ownAddress(request.account, entry.service, ctx);
|
|
12232
|
+
return { recipients, accountDomains: address ? [domainOf(address)] : void 0 };
|
|
12233
|
+
}
|
|
12234
|
+
/** Read the recipients again before a commit whose plan read them, and refuse when the policy now denies or someone was added. */
|
|
12235
|
+
async recheck(plan2, ctx) {
|
|
12236
|
+
const recheck = plan2.recheck;
|
|
12237
|
+
const resolved = this.resolve(recheck.request);
|
|
12238
|
+
const category = plan2.category;
|
|
12239
|
+
const facts = await this.policyFacts(resolved, category, recipientPlan(resolved.entry.id, category, resolved.request), ctx);
|
|
12240
|
+
const decision = evaluatePolicy(this.runtime.config, resolved.entry, category, resolved.request, facts);
|
|
12241
|
+
if (decision.effect === "deny") throw policyError(decision, plan2.account);
|
|
12242
|
+
const planned = new Set(recheck.recipients);
|
|
12243
|
+
const added = [...facts.recipients.addresses, ...facts.recipients.domains].filter((recipient) => !planned.has(recipient));
|
|
12244
|
+
if (added.length > 0) {
|
|
12245
|
+
throw new GwsError("PLAN_EXPIRED", `the recipients changed after the plan was created (added: ${added.join(", ")})`, { next: "Plan again and show the new preview." });
|
|
12246
|
+
}
|
|
12247
|
+
}
|
|
12248
|
+
/** One recipient read; undefined when it fails or answers in an unexpected shape. */
|
|
12249
|
+
async lookup(lookup, account2, ctx, self) {
|
|
12250
|
+
try {
|
|
12251
|
+
return lookup.extract(await this.readJson(lookup.operation, lookup.params, account2, ctx), self);
|
|
12252
|
+
} catch (error2) {
|
|
12253
|
+
throwIfCancelled(ctx.signal);
|
|
12254
|
+
this.runtime.assertActive(ctx.signal);
|
|
12255
|
+
if (GwsError.from(error2) === void 0) throw error2;
|
|
12256
|
+
return void 0;
|
|
12257
|
+
}
|
|
12258
|
+
}
|
|
12259
|
+
/** The account's own address, read with the operation's service first; undefined when no read gives it. */
|
|
12260
|
+
async ownAddress(account2, service, ctx) {
|
|
12261
|
+
const cached3 = this.addresses.get(account2);
|
|
12262
|
+
if (cached3 && Date.now() - cached3.at < ADDRESS_TTL_MS) return cached3.address;
|
|
12263
|
+
const enabled = this.runtime.config.accounts[account2]?.enabledServices ?? [];
|
|
12264
|
+
const services = [.../* @__PURE__ */ new Set([service, ...Object.keys(ACCOUNT_ADDRESS_READS)])].filter((name) => Object.hasOwn(ACCOUNT_ADDRESS_READS, name) && enabled.includes(name));
|
|
12265
|
+
for (const name of services) {
|
|
12266
|
+
const read = ACCOUNT_ADDRESS_READS[name];
|
|
12267
|
+
try {
|
|
12268
|
+
const address = read.extract(await this.readJson(read.operation, read.params, account2, ctx));
|
|
12269
|
+
if (address) {
|
|
12270
|
+
this.addresses.set(account2, { address, at: Date.now() });
|
|
12271
|
+
return address;
|
|
12272
|
+
}
|
|
12273
|
+
} catch (error2) {
|
|
12274
|
+
throwIfCancelled(ctx.signal);
|
|
12275
|
+
this.runtime.assertActive(ctx.signal);
|
|
12276
|
+
if (GwsError.from(error2) === void 0) throw error2;
|
|
12277
|
+
}
|
|
12278
|
+
}
|
|
12279
|
+
return void 0;
|
|
12280
|
+
}
|
|
12281
|
+
/** A field-masked read the plugin makes for itself, under the read policy; its parsed JSON. */
|
|
12282
|
+
async readJson(operation, params, account2, ctx) {
|
|
12283
|
+
const entry = commandIndex().methods.get(operation);
|
|
12284
|
+
if (!entry) throw new GwsError("OPERATION_UNKNOWN", `'${operation}' is not in the gws manifest`);
|
|
12285
|
+
const request = { account: account2, operation, params };
|
|
12286
|
+
const decision = evaluatePolicy(this.runtime.config, entry, "read", request, { recipients: recipientPlan(operation, "read", request).known });
|
|
12287
|
+
if (decision.effect === "deny") throw policyError(decision, account2);
|
|
12288
|
+
const argv = buildCommand(entry, request, this.runtime.config, { dryRun: false, category: "read" }).argv;
|
|
12289
|
+
const result = await this.accounts.run(argv, account2, operation, ctx, { scopes: entry.scopes });
|
|
12290
|
+
this.runtime.assertActive(ctx.signal);
|
|
12291
|
+
const parsed = parseResult(result, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
12292
|
+
if (!parsed.ok) throw processError(result, parsed, entry.service, this.accounts.remediation(account2, entry.scopes, entry.service));
|
|
12293
|
+
return parsed.data;
|
|
12294
|
+
}
|
|
11671
12295
|
};
|
|
11672
12296
|
function fileLimits(decision) {
|
|
11673
12297
|
const { maxUploadBytes, maxDownloadBytes, readRoots, writeRoots } = decision.constraints;
|
|
@@ -11702,10 +12326,9 @@ var Runtime = class _Runtime {
|
|
|
11702
12326
|
this.version = version2;
|
|
11703
12327
|
this.binary = binary;
|
|
11704
12328
|
this.paths = options.paths ?? pluginPaths();
|
|
11705
|
-
this.workDir =
|
|
11706
|
-
|
|
11707
|
-
this.
|
|
11708
|
-
this.schema = new SchemaClient(binary, this.workDir, localEnv(this.workDir, ownedDir(join8(this.paths.cacheDir, "discovery"))), config2.execution.schemaTimeoutMs);
|
|
12329
|
+
this.workDir = createWorkDir();
|
|
12330
|
+
this.audit = new Audit({ ...config2.audit, path: config2.audit.path ?? join9(this.paths.stateDir, "audit.jsonl") }, options.pluginVersion);
|
|
12331
|
+
this.schema = new SchemaClient(binary, this.workDir, localEnv(this.workDir, ownedDir(join9(this.paths.cacheDir, "discovery"))), config2.execution.schemaTimeoutMs);
|
|
11709
12332
|
this.accounts = new Accounts(this);
|
|
11710
12333
|
this.operations = new Operations(this, this.accounts);
|
|
11711
12334
|
}
|
|
@@ -11743,7 +12366,8 @@ var Runtime = class _Runtime {
|
|
|
11743
12366
|
}
|
|
11744
12367
|
static async create(config2, options) {
|
|
11745
12368
|
const binary = resolveBinary(config2, options.projectDirectory);
|
|
11746
|
-
|
|
12369
|
+
removeStaleWorkDirs();
|
|
12370
|
+
const probeDir = mkdtempSync2(join9(tmpdir2(), "opencode-gws-probe-"));
|
|
11747
12371
|
let version2 = "unknown";
|
|
11748
12372
|
try {
|
|
11749
12373
|
const probe = await runProcess({
|
|
@@ -11760,7 +12384,7 @@ var Runtime = class _Runtime {
|
|
|
11760
12384
|
throw new Error(`GWS_VERSION_MISMATCH: Requires ${GWS_VERSION} and the matching manifest`);
|
|
11761
12385
|
}
|
|
11762
12386
|
} finally {
|
|
11763
|
-
|
|
12387
|
+
rmSync2(probeDir, { recursive: true, force: true });
|
|
11764
12388
|
}
|
|
11765
12389
|
return new _Runtime(config2, options, binary, version2);
|
|
11766
12390
|
}
|
|
@@ -11824,7 +12448,7 @@ var Runtime = class _Runtime {
|
|
|
11824
12448
|
this.plans.invalidateAll();
|
|
11825
12449
|
this.audit.close();
|
|
11826
12450
|
this.closing = settled.then(() => {
|
|
11827
|
-
|
|
12451
|
+
rmSync2(this.workDir, { recursive: true, force: true });
|
|
11828
12452
|
});
|
|
11829
12453
|
return this.closing;
|
|
11830
12454
|
}
|
|
@@ -11847,8 +12471,7 @@ async function start(ctx) {
|
|
|
11847
12471
|
const ids = integrationIds(config2.accounts);
|
|
11848
12472
|
const runtime = await Runtime.create(config2, {
|
|
11849
12473
|
pluginVersion: PLUGIN_VERSION,
|
|
11850
|
-
|
|
11851
|
-
hostWarning: host.warning,
|
|
12474
|
+
host,
|
|
11852
12475
|
projectDirectory: ctx.location.directory,
|
|
11853
12476
|
tokenProvider: integrationTokenProvider(ctx, config2, ids),
|
|
11854
12477
|
connection: integrationConnection(ctx, ids),
|