@joe-broadhead/opencode-gws 0.0.2 → 0.0.3
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 +28 -0
- package/README.md +24 -6
- package/dist/index.js +972 -352
- package/dist/tui.js +10 -9
- package/package.json +7 -6
- 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, 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.3";
|
|
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,390 @@ 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()));
|
|
10859
11077
|
}
|
|
10860
11078
|
};
|
|
10861
|
-
visit(request.params);
|
|
10862
|
-
visit(request.body);
|
|
10863
|
-
|
|
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
|
+
};
|
|
10864
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
|
+
extract(data) {
|
|
11189
|
+
const found = headers(record2(data)["message"]);
|
|
11190
|
+
return found ? setOf([...found.get("to") ?? [], ...found.get("cc") ?? [], ...found.get("bcc") ?? []]) : void 0;
|
|
11191
|
+
}
|
|
11192
|
+
};
|
|
11193
|
+
return plan(out, [lookup]);
|
|
11194
|
+
};
|
|
11195
|
+
function attendeesInto(out, body) {
|
|
11196
|
+
const attendees = record2(body)["attendees"];
|
|
11197
|
+
if (attendees === void 0) return;
|
|
11198
|
+
if (!Array.isArray(attendees)) {
|
|
11199
|
+
out.fail("the attendees could not be read");
|
|
11200
|
+
return;
|
|
11201
|
+
}
|
|
11202
|
+
for (const attendee of attendees) {
|
|
11203
|
+
const email2 = record2(attendee)["email"];
|
|
11204
|
+
if (typeof email2 === "string") out.text(email2);
|
|
11205
|
+
else out.fail("an attendee has no email");
|
|
11206
|
+
}
|
|
11207
|
+
}
|
|
11208
|
+
var eventInsert = (request) => {
|
|
11209
|
+
const out = new Collector();
|
|
11210
|
+
attendeesInto(out, request.body);
|
|
11211
|
+
return plan(out);
|
|
11212
|
+
};
|
|
11213
|
+
var eventChange = (request, category) => {
|
|
11214
|
+
const out = new Collector();
|
|
11215
|
+
attendeesInto(out, request.body);
|
|
11216
|
+
if (category !== "send") return plan(out);
|
|
11217
|
+
const params = record2(request.params);
|
|
11218
|
+
const calendarId = text3(params["calendarId"]);
|
|
11219
|
+
const eventId = text3(params["eventId"]);
|
|
11220
|
+
if (!calendarId || !eventId) {
|
|
11221
|
+
out.fail("the event is not named (calendarId and eventId)");
|
|
11222
|
+
return plan(out);
|
|
11223
|
+
}
|
|
11224
|
+
const lookup = {
|
|
11225
|
+
operation: "calendar.events.get",
|
|
11226
|
+
params: { calendarId, eventId, fields: "attendees(email)" },
|
|
11227
|
+
what: "the event's guests",
|
|
11228
|
+
extract(data) {
|
|
11229
|
+
const attendees = record2(data)["attendees"] ?? [];
|
|
11230
|
+
if (!Array.isArray(attendees)) return void 0;
|
|
11231
|
+
const emails2 = attendees.map((attendee) => record2(attendee)["email"]);
|
|
11232
|
+
return emails2.every((email2) => typeof email2 === "string") ? setOf(emails2) : void 0;
|
|
11233
|
+
}
|
|
11234
|
+
};
|
|
11235
|
+
return plan(out, [lookup]);
|
|
11236
|
+
};
|
|
11237
|
+
var quickAdd = () => {
|
|
11238
|
+
const out = new Collector();
|
|
11239
|
+
out.fail("Google reads the guests from the text");
|
|
11240
|
+
return plan(out);
|
|
11241
|
+
};
|
|
11242
|
+
function drivePermissionInto(out, permission) {
|
|
11243
|
+
const type = text3(permission["type"]);
|
|
11244
|
+
if (type === "anyone") return;
|
|
11245
|
+
if (type === "domain") out.domain(permission["domain"]);
|
|
11246
|
+
else if (typeof permission["emailAddress"] === "string") out.text(permission["emailAddress"]);
|
|
11247
|
+
else out.fail("the permission names no emailAddress");
|
|
11248
|
+
}
|
|
11249
|
+
var drivePermissionCreate = (request) => {
|
|
11250
|
+
const out = new Collector();
|
|
11251
|
+
drivePermissionInto(out, record2(request.body));
|
|
11252
|
+
return plan(out);
|
|
11253
|
+
};
|
|
11254
|
+
var drivePermissionUpdate = (request) => {
|
|
11255
|
+
const out = new Collector();
|
|
11256
|
+
const params = record2(request.params);
|
|
11257
|
+
const fileId = text3(params["fileId"]);
|
|
11258
|
+
const permissionId = text3(params["permissionId"]);
|
|
11259
|
+
if (!fileId || !permissionId) {
|
|
11260
|
+
out.fail("the permission is not named (fileId and permissionId)");
|
|
11261
|
+
return plan(out);
|
|
11262
|
+
}
|
|
11263
|
+
const lookup = {
|
|
11264
|
+
operation: "drive.permissions.get",
|
|
11265
|
+
// The request's own parameters, so a shared-drive flag it carries applies to the read too.
|
|
11266
|
+
params: { ...params, fields: "type,emailAddress,domain" },
|
|
11267
|
+
what: "the permission",
|
|
11268
|
+
extract(data) {
|
|
11269
|
+
const found = new Collector();
|
|
11270
|
+
drivePermissionInto(found, record2(data));
|
|
11271
|
+
return found.unknown ? void 0 : found.result();
|
|
11272
|
+
}
|
|
11273
|
+
};
|
|
11274
|
+
return plan(out, [lookup]);
|
|
11275
|
+
};
|
|
11276
|
+
function aclScopeInto(out, type, value) {
|
|
11277
|
+
if (type === "default") out.public = true;
|
|
11278
|
+
else if (type === "domain") out.domain(value);
|
|
11279
|
+
else if ((type === "user" || type === "group") && typeof value === "string") out.text(value);
|
|
11280
|
+
else out.fail("the ACL scope could not be read");
|
|
11281
|
+
}
|
|
11282
|
+
var aclChange = (request) => {
|
|
11283
|
+
const out = new Collector();
|
|
11284
|
+
const scope = record2(record2(request.body)["scope"]);
|
|
11285
|
+
if (Object.keys(scope).length > 0) aclScopeInto(out, text3(scope["type"]), scope["value"]);
|
|
11286
|
+
const ruleId = text3(record2(request.params)["ruleId"]);
|
|
11287
|
+
if (ruleId) {
|
|
11288
|
+
const colon = ruleId.indexOf(":");
|
|
11289
|
+
aclScopeInto(out, colon < 0 ? ruleId : ruleId.slice(0, colon), colon < 0 ? void 0 : ruleId.slice(colon + 1));
|
|
11290
|
+
}
|
|
11291
|
+
if (Object.keys(scope).length === 0 && !ruleId) out.fail("the ACL scope could not be read");
|
|
11292
|
+
return plan(out);
|
|
11293
|
+
};
|
|
11294
|
+
function bodyAddress(field, optional2) {
|
|
11295
|
+
return (request) => {
|
|
11296
|
+
const out = new Collector();
|
|
11297
|
+
const body = record2(request.body);
|
|
11298
|
+
if (optional2?.(body)) return plan(out);
|
|
11299
|
+
const value = body[field];
|
|
11300
|
+
if (typeof value === "string" && value.includes("@")) out.text(value);
|
|
11301
|
+
else out.fail(`body.${field} is not an email address`);
|
|
11302
|
+
return plan(out);
|
|
11303
|
+
};
|
|
11304
|
+
}
|
|
11305
|
+
var chatMember = (request) => {
|
|
11306
|
+
const out = new Collector();
|
|
11307
|
+
const name = text3(record2(record2(request.body)["member"])["name"]);
|
|
11308
|
+
if (name?.startsWith("users/") && name.includes("@")) out.text(name.slice("users/".length));
|
|
11309
|
+
else out.fail("the member is not named by email (users/<email>)");
|
|
11310
|
+
return plan(out);
|
|
11311
|
+
};
|
|
11312
|
+
var keepPermissions = (request) => {
|
|
11313
|
+
const out = new Collector();
|
|
11314
|
+
const requests = record2(request.body)["requests"];
|
|
11315
|
+
if (!Array.isArray(requests) || requests.length === 0) out.fail("the permission requests could not be read");
|
|
11316
|
+
else {
|
|
11317
|
+
for (const item of requests) {
|
|
11318
|
+
const email2 = record2(record2(item)["permission"])["email"];
|
|
11319
|
+
if (typeof email2 === "string") out.text(email2);
|
|
11320
|
+
else out.fail("a permission names no email");
|
|
11321
|
+
}
|
|
11322
|
+
}
|
|
11323
|
+
return plan(out);
|
|
11324
|
+
};
|
|
11325
|
+
var revoke = () => plan(new Collector());
|
|
11326
|
+
var SPECS = {
|
|
11327
|
+
"gmail.send": helperSend(["to", "cc", "bcc"]),
|
|
11328
|
+
"gmail.forward": helperSend(["to", "cc", "bcc"]),
|
|
11329
|
+
"gmail.reply": replySpec(false),
|
|
11330
|
+
"gmail.reply-all": replySpec(true),
|
|
11331
|
+
"gmail.users.messages.send": messagesSend,
|
|
11332
|
+
"gmail.users.drafts.send": draftsSend,
|
|
11333
|
+
"gmail.users.settings.delegates.create": bodyAddress("delegateEmail"),
|
|
11334
|
+
"gmail.users.settings.forwardingAddresses.create": bodyAddress("forwardingEmail"),
|
|
11335
|
+
"gmail.users.settings.updateAutoForwarding": bodyAddress("emailAddress", (body) => body["enabled"] === false),
|
|
11336
|
+
"gmail.users.settings.delegates.delete": revoke,
|
|
11337
|
+
"gmail.users.settings.forwardingAddresses.delete": revoke,
|
|
11338
|
+
"calendar.events.insert": eventInsert,
|
|
11339
|
+
"calendar.events.import": eventInsert,
|
|
11340
|
+
"calendar.events.quickAdd": quickAdd,
|
|
11341
|
+
"calendar.events.update": eventChange,
|
|
11342
|
+
"calendar.events.patch": eventChange,
|
|
11343
|
+
"calendar.events.delete": eventChange,
|
|
11344
|
+
"calendar.events.move": eventChange,
|
|
11345
|
+
"calendar.acl.insert": aclChange,
|
|
11346
|
+
"calendar.acl.update": aclChange,
|
|
11347
|
+
"calendar.acl.patch": aclChange,
|
|
11348
|
+
"calendar.acl.delete": revoke,
|
|
11349
|
+
"drive.permissions.create": drivePermissionCreate,
|
|
11350
|
+
"drive.permissions.update": drivePermissionUpdate,
|
|
11351
|
+
"drive.permissions.delete": revoke,
|
|
11352
|
+
"chat.spaces.members.create": chatMember,
|
|
11353
|
+
"chat.spaces.members.delete": revoke,
|
|
11354
|
+
"classroom.invitations.create": bodyAddress("userId"),
|
|
11355
|
+
"classroom.invitations.delete": revoke,
|
|
11356
|
+
"classroom.courses.studentGroups.studentGroupMembers.create": bodyAddress("userId"),
|
|
11357
|
+
"meet.spaces.members.create": bodyAddress("email"),
|
|
11358
|
+
"keep.notes.permissions.batchCreate": keepPermissions,
|
|
11359
|
+
"keep.notes.permissions.batchDelete": revoke
|
|
11360
|
+
};
|
|
11361
|
+
function recipientPlan(operation, category, request) {
|
|
11362
|
+
const spec2 = SPECS[operation];
|
|
11363
|
+
if (spec2) return spec2(request, category);
|
|
11364
|
+
const scanned = scanRecipients(request);
|
|
11365
|
+
if (category === "send" || category === "share") {
|
|
11366
|
+
return { known: { ...scanned, unknown: scanned.unknown ?? `the plugin cannot tell who ${operation} reaches` }, lookups: [] };
|
|
11367
|
+
}
|
|
11368
|
+
return { known: scanned, lookups: [] };
|
|
11369
|
+
}
|
|
11370
|
+
function withLookup(recipients, found, what) {
|
|
11371
|
+
if (!found) return { ...recipients, unknown: recipients.unknown ?? `${what} could not be read` };
|
|
11372
|
+
return {
|
|
11373
|
+
...recipients,
|
|
11374
|
+
addresses: [.../* @__PURE__ */ new Set([...recipients.addresses, ...found.addresses])],
|
|
11375
|
+
domains: [.../* @__PURE__ */ new Set([...recipients.domains, ...found.domains])]
|
|
11376
|
+
};
|
|
11377
|
+
}
|
|
11378
|
+
function recipientLabels(recipients) {
|
|
11379
|
+
return [...recipients.addresses, ...recipients.domains.map((domain) => `*@${domain}`)];
|
|
11380
|
+
}
|
|
11381
|
+
var ACCOUNT_ADDRESS_READS = {
|
|
11382
|
+
gmail: { operation: "gmail.users.getProfile", params: { userId: "me", fields: "emailAddress" }, extract: (data) => ownAddress(record2(data)["emailAddress"]) },
|
|
11383
|
+
drive: { operation: "drive.about.get", params: { fields: "user(emailAddress)" }, extract: (data) => ownAddress(record2(record2(data)["user"])["emailAddress"]) },
|
|
11384
|
+
calendar: { operation: "calendar.calendars.get", params: { calendarId: "primary", fields: "id" }, extract: (data) => ownAddress(record2(data)["id"]) }
|
|
11385
|
+
};
|
|
11386
|
+
function ownAddress(value) {
|
|
11387
|
+
if (typeof value !== "string") return void 0;
|
|
11388
|
+
const found = addressesIn(value);
|
|
11389
|
+
return found.addresses.length === 1 && found.unreadable.length === 0 ? found.addresses[0] : void 0;
|
|
11390
|
+
}
|
|
11391
|
+
|
|
11392
|
+
// src/domain/command.ts
|
|
10865
11393
|
function wantsPages(request) {
|
|
10866
11394
|
const pagination = request.pagination ?? {};
|
|
10867
11395
|
return pagination.all === true || pagination.maxPages !== void 0 || pagination.maxItems !== void 0;
|
|
@@ -10940,7 +11468,7 @@ function buildCommand(entry, request, config2, options) {
|
|
|
10940
11468
|
url: entry.pathTemplate,
|
|
10941
11469
|
params: redactParams(params),
|
|
10942
11470
|
bodySummary: request.body === void 0 ? void 0 : summarizeBody(request.body),
|
|
10943
|
-
recipients:
|
|
11471
|
+
recipients: recipientLabels(scanRecipients(request)),
|
|
10944
11472
|
uploads: request.upload ? [request.upload.path] : void 0,
|
|
10945
11473
|
destination: request.download?.path,
|
|
10946
11474
|
overwrite: request.download?.overwrite === true ? true : void 0,
|
|
@@ -10978,32 +11506,6 @@ function matches2(rule, category, account2, service, operation) {
|
|
|
10978
11506
|
function underRoot2(path, roots) {
|
|
10979
11507
|
return roots.some((root) => underRoot(path, root));
|
|
10980
11508
|
}
|
|
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
11509
|
function helperPaths(request, keys) {
|
|
11008
11510
|
const input = request.input;
|
|
11009
11511
|
if (!input || typeof input !== "object") return [];
|
|
@@ -11021,13 +11523,19 @@ function publicSharing(request) {
|
|
|
11021
11523
|
const haystack = JSON.stringify({ params: request.params ?? {}, body: request.body ?? {}, input: request.input ?? {} }).toLowerCase();
|
|
11022
11524
|
return /"role"\s*:\s*"(anyone|public)"/.test(haystack) || /"type"\s*:\s*"(anyone|public)"/.test(haystack);
|
|
11023
11525
|
}
|
|
11024
|
-
function
|
|
11025
|
-
return
|
|
11526
|
+
function limitsRecipients(rule) {
|
|
11527
|
+
return rule.allowedRecipientDomains !== void 0 || rule.allowedRecipients !== void 0 || rule.deniedRecipientDomains !== void 0 || rule.allowExternalRecipients === false;
|
|
11528
|
+
}
|
|
11529
|
+
function matching(config2, entry, category, request) {
|
|
11530
|
+
return config2.policy.rules.filter((rule) => matches2(rule, category, request.account, entry.service, entry.id));
|
|
11531
|
+
}
|
|
11532
|
+
function needsAccountDomains(config2, entry, category, request) {
|
|
11533
|
+
return matching(config2, entry, category, request).some((rule) => rule.allowExternalRecipients === false);
|
|
11026
11534
|
}
|
|
11027
11535
|
function constrained(rule) {
|
|
11028
11536
|
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
11537
|
}
|
|
11030
|
-
function evaluatePolicy(config2, entry, category, request) {
|
|
11538
|
+
function evaluatePolicy(config2, entry, category, request, facts) {
|
|
11031
11539
|
const constraints = {
|
|
11032
11540
|
maxPages: config2.execution.maxPages,
|
|
11033
11541
|
maxItems: config2.execution.maxItems,
|
|
@@ -11037,31 +11545,21 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11037
11545
|
writeRoots: config2.execution.allowedWriteRoots.length > 0 ? [config2.execution.allowedWriteRoots] : []
|
|
11038
11546
|
};
|
|
11039
11547
|
const violations = [];
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
if (!config2.accounts[request.account]) {
|
|
11044
|
-
return { effect: "deny", violations: ["account-unknown"], constraints };
|
|
11045
|
-
}
|
|
11548
|
+
const refuse = (violation) => ({ effect: "deny", violations: [violation], constraints, code: "POLICY_DENIED" });
|
|
11549
|
+
if (entry.category === "deny" || entry.schemaError) return refuse("manifest-deny");
|
|
11550
|
+
if (!config2.accounts[request.account]) return refuse("account-unknown");
|
|
11046
11551
|
const profile = config2.accounts[request.account];
|
|
11047
11552
|
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
|
-
}
|
|
11553
|
+
if (!profile.enabledServices.includes(service)) return refuse("service-disabled");
|
|
11554
|
+
if (category === "deploy" || category === "watch" || category === "deny") return refuse("never-committable");
|
|
11054
11555
|
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
|
-
}
|
|
11556
|
+
if (!config2.admin.enabled) return refuse("admin-disabled");
|
|
11557
|
+
if (!config2.admin.allowedServices.includes(service)) return refuse("admin-service-not-allowed");
|
|
11061
11558
|
}
|
|
11062
11559
|
let denied2 = false;
|
|
11063
11560
|
let ruleId;
|
|
11064
|
-
|
|
11561
|
+
let unknownRecipients = 0;
|
|
11562
|
+
let unknownDomain = 0;
|
|
11065
11563
|
const readPaths = [
|
|
11066
11564
|
...request.upload?.path ? [request.upload.path] : [],
|
|
11067
11565
|
...helperPaths(request, ["path", "file", "filePath", "source", "attachments"])
|
|
@@ -11070,28 +11568,44 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11070
11568
|
...request.download?.path ? [request.download.path] : [],
|
|
11071
11569
|
...helperPaths(request, ["destination", "output", "outPath"])
|
|
11072
11570
|
];
|
|
11073
|
-
const
|
|
11074
|
-
const
|
|
11075
|
-
|
|
11076
|
-
|
|
11571
|
+
const recipients = facts === "pending" ? void 0 : facts.recipients;
|
|
11572
|
+
const targets = recipients ? [
|
|
11573
|
+
...recipients.addresses.map((address) => ({ label: address, address, domain: domainOf(address) })),
|
|
11574
|
+
...recipients.domains.map((domain) => ({ label: `*@${domain}`, address: void 0, domain }))
|
|
11575
|
+
] : [];
|
|
11576
|
+
const own2 = facts === "pending" || !facts.accountDomains ? void 0 : new Set(facts.accountDomains.map((value) => value.toLowerCase()));
|
|
11577
|
+
const isPublic = publicSharing(request) || recipients?.public === true;
|
|
11578
|
+
for (const rule of matching(config2, entry, category, request)) {
|
|
11077
11579
|
const before = violations.length;
|
|
11078
|
-
if (
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11580
|
+
if (recipients && limitsRecipients(rule)) {
|
|
11581
|
+
if (recipients.unknown) {
|
|
11582
|
+
violations.push(`rule ${rule.id} limits recipients, but they could not be determined: ${recipients.unknown}`);
|
|
11583
|
+
unknownRecipients++;
|
|
11584
|
+
}
|
|
11585
|
+
if (rule.allowedRecipientDomains) {
|
|
11586
|
+
const allowed = new Set(rule.allowedRecipientDomains.map((value) => value.toLowerCase()));
|
|
11587
|
+
const bad = targets.filter((target) => !allowed.has(target.domain));
|
|
11588
|
+
if (bad.length > 0) violations.push(`recipients outside allowed domains: ${bad.map((target) => target.label).join(", ")}`);
|
|
11589
|
+
}
|
|
11590
|
+
if (rule.allowedRecipients) {
|
|
11591
|
+
const allowed = new Set(rule.allowedRecipients.map((value) => value.trim().toLowerCase()));
|
|
11592
|
+
const bad = targets.filter((target) => target.address === void 0 || !allowed.has(target.address));
|
|
11593
|
+
if (bad.length > 0) violations.push(`recipients not in allowedRecipients: ${bad.map((target) => target.label).join(", ")}`);
|
|
11594
|
+
}
|
|
11595
|
+
if (rule.deniedRecipientDomains) {
|
|
11596
|
+
const deniedDomains = new Set(rule.deniedRecipientDomains.map((value) => value.toLowerCase()));
|
|
11597
|
+
const bad = targets.filter((target) => deniedDomains.has(target.domain));
|
|
11598
|
+
if (bad.length > 0) violations.push(`recipients in denied domains: ${bad.map((target) => target.label).join(", ")}`);
|
|
11599
|
+
}
|
|
11600
|
+
if (rule.allowExternalRecipients === false && (targets.length > 0 || recipients.unknown)) {
|
|
11601
|
+
if (!own2) {
|
|
11602
|
+
violations.push(`rule ${rule.id} disables external recipients, but the account's own domain is unknown`);
|
|
11603
|
+
unknownDomain++;
|
|
11604
|
+
} else {
|
|
11605
|
+
const external = targets.filter((target) => !own2.has(target.domain));
|
|
11606
|
+
if (external.length > 0) violations.push(`external recipients are disabled: ${external.map((target) => target.label).join(", ")}`);
|
|
11607
|
+
}
|
|
11608
|
+
}
|
|
11095
11609
|
}
|
|
11096
11610
|
if (rule.allowPublicSharing === false && isPublic) {
|
|
11097
11611
|
violations.push("public sharing is disabled");
|
|
@@ -11129,16 +11643,18 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11129
11643
|
violations.push("admin plans require a non-empty reason");
|
|
11130
11644
|
}
|
|
11131
11645
|
if (violations.length > 0 || denied2) {
|
|
11132
|
-
|
|
11646
|
+
const definite = violations.length - unknownRecipients - unknownDomain;
|
|
11647
|
+
const code2 = definite > 0 || violations.length === 0 ? "POLICY_DENIED" : unknownRecipients > 0 ? "RECIPIENTS_UNKNOWN" : "ACCOUNT_DOMAIN_UNKNOWN";
|
|
11648
|
+
return { effect: "deny", ruleId, violations: violations.length > 0 ? violations : [`denied by rule ${ruleId ?? "policy"}`], constraints, code: code2 };
|
|
11133
11649
|
}
|
|
11134
|
-
return { effect: "allow", ruleId, violations, constraints };
|
|
11650
|
+
return { effect: "allow", ruleId, violations, constraints, code: "POLICY_DENIED" };
|
|
11135
11651
|
}
|
|
11136
11652
|
|
|
11137
11653
|
// src/process/staging.ts
|
|
11138
11654
|
import { createHash as createHash3 } from "node:crypto";
|
|
11139
11655
|
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
|
|
11656
|
+
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";
|
|
11657
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute7, join as join8 } from "node:path";
|
|
11142
11658
|
var CHUNK = 64 * 1024;
|
|
11143
11659
|
var READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
11144
11660
|
var Stage = class _Stage {
|
|
@@ -11150,13 +11666,13 @@ var Stage = class _Stage {
|
|
|
11150
11666
|
signal;
|
|
11151
11667
|
static async create(workDir, signal) {
|
|
11152
11668
|
throwIfCancelled(signal);
|
|
11153
|
-
const dir = await
|
|
11669
|
+
const dir = await mkdtemp2(join8(workDir, "call-"));
|
|
11154
11670
|
try {
|
|
11155
11671
|
throwIfCancelled(signal);
|
|
11156
|
-
await writeFile2(
|
|
11672
|
+
await writeFile2(join8(dir, ".env"), "", { mode: 384 });
|
|
11157
11673
|
return new _Stage(dir, signal);
|
|
11158
11674
|
} catch (error2) {
|
|
11159
|
-
await
|
|
11675
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
11160
11676
|
throw error2;
|
|
11161
11677
|
}
|
|
11162
11678
|
}
|
|
@@ -11170,7 +11686,7 @@ var Stage = class _Stage {
|
|
|
11170
11686
|
/** Create the directory gws writes the output into. */
|
|
11171
11687
|
async prepareOutput(output) {
|
|
11172
11688
|
throwIfCancelled(this.signal);
|
|
11173
|
-
await mkdir2(dirname5(
|
|
11689
|
+
await mkdir2(dirname5(join8(this.dir, output.rel)), { recursive: true, mode: 448 });
|
|
11174
11690
|
}
|
|
11175
11691
|
/**
|
|
11176
11692
|
* Move the staged output to its destination: size cap, then the same
|
|
@@ -11180,7 +11696,7 @@ var Stage = class _Stage {
|
|
|
11180
11696
|
*/
|
|
11181
11697
|
async deliver(output, limits) {
|
|
11182
11698
|
throwIfCancelled(this.signal);
|
|
11183
|
-
const staged =
|
|
11699
|
+
const staged = join8(this.dir, output.rel);
|
|
11184
11700
|
const info = await lstat(staged).catch(() => void 0);
|
|
11185
11701
|
if (!info?.isFile()) throw new GwsError("DOWNLOAD_INVALID", "gws did not write the output file", { next: "Report it; the plan is used up." });
|
|
11186
11702
|
if (info.size > limits.maxDownloadBytes) {
|
|
@@ -11192,7 +11708,7 @@ var Stage = class _Stage {
|
|
|
11192
11708
|
return { path: output.destination, bytes: info.size, sha256, ...cleanupDegraded ? { cleanupDegraded: true } : {} };
|
|
11193
11709
|
}
|
|
11194
11710
|
async dispose() {
|
|
11195
|
-
await
|
|
11711
|
+
await rm2(this.dir, { recursive: true, force: true });
|
|
11196
11712
|
}
|
|
11197
11713
|
async stageInput(input, maxBytes, roots) {
|
|
11198
11714
|
throwIfCancelled(this.signal);
|
|
@@ -11204,7 +11720,7 @@ var Stage = class _Stage {
|
|
|
11204
11720
|
next: "Use a file inside execution.allowedReadRoots, or ask the user to add its folder there."
|
|
11205
11721
|
});
|
|
11206
11722
|
}
|
|
11207
|
-
if (!(await
|
|
11723
|
+
if (!(await stat2(real)).isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11208
11724
|
let source;
|
|
11209
11725
|
let target;
|
|
11210
11726
|
try {
|
|
@@ -11212,7 +11728,7 @@ var Stage = class _Stage {
|
|
|
11212
11728
|
const info = await source.stat();
|
|
11213
11729
|
if (!info.isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11214
11730
|
if (info.size > maxBytes) throw uploadLimit(input.source, info.size, maxBytes);
|
|
11215
|
-
const destination =
|
|
11731
|
+
const destination = join8(this.dir, input.rel);
|
|
11216
11732
|
throwIfCancelled(this.signal);
|
|
11217
11733
|
await mkdir2(dirname5(destination), { recursive: true, mode: 448 });
|
|
11218
11734
|
throwIfCancelled(this.signal);
|
|
@@ -11242,7 +11758,7 @@ async function checkDestination(output, limits) {
|
|
|
11242
11758
|
if (!isAbsolute7(output.destination)) throw new GwsError("DOWNLOAD_INVALID", `'${output.destination}' must be an absolute path`);
|
|
11243
11759
|
const parent = await realpath(dirname5(output.destination)).catch(() => void 0);
|
|
11244
11760
|
if (!parent) throw new GwsError("DOWNLOAD_INVALID", `the directory for '${output.destination}' does not exist`);
|
|
11245
|
-
const target =
|
|
11761
|
+
const target = join8(parent, basename2(output.destination));
|
|
11246
11762
|
if (!withinRootSets(target, await realRootSets(limits.writeRoots))) {
|
|
11247
11763
|
throw new GwsError("POLICY_DENIED", `download path '${output.destination}' resolves outside allowed write roots`, {
|
|
11248
11764
|
next: "Use a destination inside execution.allowedWriteRoots, or ask the user to add its folder there."
|
|
@@ -11256,20 +11772,20 @@ async function checkDestination(output, limits) {
|
|
|
11256
11772
|
async function place(staged, target, output, signal) {
|
|
11257
11773
|
throwIfCancelled(signal);
|
|
11258
11774
|
try {
|
|
11259
|
-
if (output.overwrite) await
|
|
11775
|
+
if (output.overwrite) await rename2(staged, target);
|
|
11260
11776
|
else await link(staged, target);
|
|
11261
11777
|
return false;
|
|
11262
11778
|
} catch (error2) {
|
|
11263
11779
|
if (code(error2) === "EEXIST") throw exists(output.destination);
|
|
11264
11780
|
if (code(error2) !== "EXDEV" && code(error2) !== "EPERM" && code(error2) !== "ENOTSUP") throw error2;
|
|
11265
11781
|
}
|
|
11266
|
-
const temp =
|
|
11782
|
+
const temp = join8(dirname5(target), `.${basename2(target)}.${process.pid}.${Date.now()}.partial`);
|
|
11267
11783
|
let cleanupDegraded = false;
|
|
11268
11784
|
try {
|
|
11269
11785
|
throwIfCancelled(signal);
|
|
11270
11786
|
await copyDurable(staged, temp, signal);
|
|
11271
11787
|
throwIfCancelled(signal);
|
|
11272
|
-
if (output.overwrite) await
|
|
11788
|
+
if (output.overwrite) await rename2(temp, target);
|
|
11273
11789
|
else {
|
|
11274
11790
|
try {
|
|
11275
11791
|
await link(temp, target);
|
|
@@ -11338,6 +11854,17 @@ function code(error2) {
|
|
|
11338
11854
|
// src/runtime/operations.ts
|
|
11339
11855
|
var METHOD_FIELDS = ["params", "body", "fields", "pagination", "upload", "download"];
|
|
11340
11856
|
var PLAN_INSTEAD = "Call gws_plan with the same request, then commit with the tool named by the plan's category.";
|
|
11857
|
+
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.";
|
|
11858
|
+
var ADDRESS_TTL_MS = 10 * 60 * 1e3;
|
|
11859
|
+
function policyError(decision, account2) {
|
|
11860
|
+
const detail = decision.violations.join("; ");
|
|
11861
|
+
if (decision.code === "ACCOUNT_DOMAIN_UNKNOWN") {
|
|
11862
|
+
return new GwsError("ACCOUNT_DOMAIN_UNKNOWN", detail, {
|
|
11863
|
+
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.`
|
|
11864
|
+
});
|
|
11865
|
+
}
|
|
11866
|
+
return new GwsError(decision.code, detail);
|
|
11867
|
+
}
|
|
11341
11868
|
var Operations = class {
|
|
11342
11869
|
constructor(runtime, accounts) {
|
|
11343
11870
|
this.runtime = runtime;
|
|
@@ -11345,6 +11872,8 @@ var Operations = class {
|
|
|
11345
11872
|
}
|
|
11346
11873
|
runtime;
|
|
11347
11874
|
accounts;
|
|
11875
|
+
/** Each account's own address, once read. */
|
|
11876
|
+
addresses = /* @__PURE__ */ new Map();
|
|
11348
11877
|
/** The configured account a call names, or the only one when it names none. */
|
|
11349
11878
|
account(alias) {
|
|
11350
11879
|
const aliases = Object.keys(this.runtime.config.accounts);
|
|
@@ -11416,8 +11945,9 @@ var Operations = class {
|
|
|
11416
11945
|
throw new GwsError("POLICY_DENIED", `'${entry.id}' is ${category2}; use gws_plan and the matching commit tool`, { next: PLAN_INSTEAD });
|
|
11417
11946
|
}
|
|
11418
11947
|
}
|
|
11419
|
-
const
|
|
11420
|
-
|
|
11948
|
+
const facts = { recipients: recipientPlan(entry.id, "read", request).known, accountDomains: this.runtime.config.accounts[request.account]?.domains };
|
|
11949
|
+
const decision2 = evaluatePolicy(this.runtime.config, entry, "read", request, facts);
|
|
11950
|
+
if (decision2.effect === "deny") throw policyError(decision2, request.account);
|
|
11421
11951
|
return { category: "read", decision: decision2 };
|
|
11422
11952
|
}
|
|
11423
11953
|
const category = helper ? helper.category : effectiveCategory(entry, request);
|
|
@@ -11426,8 +11956,8 @@ var Operations = class {
|
|
|
11426
11956
|
next: category === "read" ? "Run it with gws_read." : "Tell the user the plugin never runs it."
|
|
11427
11957
|
});
|
|
11428
11958
|
}
|
|
11429
|
-
const decision = evaluatePolicy(this.runtime.config, entry, category, request);
|
|
11430
|
-
if (decision.effect === "deny") throw
|
|
11959
|
+
const decision = evaluatePolicy(this.runtime.config, entry, category, request, "pending");
|
|
11960
|
+
if (decision.effect === "deny") throw policyError(decision, request.account);
|
|
11431
11961
|
if (category === "admin" && (!this.runtime.config.admin.enabled || decision.effect !== "allow")) {
|
|
11432
11962
|
throw new GwsError("POLICY_DENIED", "admin operations require admin.enabled, an allowed service, and a reason when one is required");
|
|
11433
11963
|
}
|
|
@@ -11440,11 +11970,13 @@ var Operations = class {
|
|
|
11440
11970
|
inspect(input, mode) {
|
|
11441
11971
|
const resolved = this.resolve(input);
|
|
11442
11972
|
const { category } = this.gate(resolved, mode);
|
|
11973
|
+
const reads = mode === "plan" ? recipientPlan(resolved.entry.id, category, resolved.request).lookups.map((lookup) => lookup.operation) : [];
|
|
11443
11974
|
return {
|
|
11444
11975
|
account: resolved.request.account,
|
|
11445
11976
|
operation: resolved.entry.id,
|
|
11446
11977
|
service: resolved.entry.service,
|
|
11447
|
-
category
|
|
11978
|
+
category,
|
|
11979
|
+
...reads.length > 0 ? { reads } : {}
|
|
11448
11980
|
};
|
|
11449
11981
|
}
|
|
11450
11982
|
/**
|
|
@@ -11500,7 +12032,7 @@ var Operations = class {
|
|
|
11500
12032
|
requestedInlineBytes: helper ? void 0 : request.response?.maxInlineBytes
|
|
11501
12033
|
});
|
|
11502
12034
|
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));
|
|
12035
|
+
if (failed) throw processError(result, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11504
12036
|
const pages = paged ? capPageItems(parsed.data, itemCap(request, limits)) : void 0;
|
|
11505
12037
|
const cap = inlineCap(this.runtime.config.execution.maxInlineBytes, request.response?.mode, request.response?.maxInlineBytes);
|
|
11506
12038
|
const truncated = pages ? Buffer.byteLength(JSON.stringify(pages.data)) > cap : parsed.truncated;
|
|
@@ -11517,7 +12049,7 @@ var Operations = class {
|
|
|
11517
12049
|
ok: true,
|
|
11518
12050
|
operation: entry.id,
|
|
11519
12051
|
category: "read",
|
|
11520
|
-
data: helper ? parsed.data ?? parsed.raw ?? result.stdout.toString("utf8") : pages ? pages.data : parsed.data,
|
|
12052
|
+
data: helper ? parsed.data ?? parsed.raw ?? result.stdout.toString("utf8") : pages ? pages.data : withoutSavedBody(parsed.data, SAVE_INSTEAD),
|
|
11521
12053
|
truncated,
|
|
11522
12054
|
diagnostics: parsed.diagnostics,
|
|
11523
12055
|
pagination: pages ? { pages: pages.pages, items: pages.items, capped: pages.capped } : void 0,
|
|
@@ -11532,8 +12064,12 @@ var Operations = class {
|
|
|
11532
12064
|
const resolved = this.resolve(input);
|
|
11533
12065
|
const { entry, helper, request } = resolved;
|
|
11534
12066
|
const started = Date.now();
|
|
11535
|
-
const { category
|
|
12067
|
+
const { category } = this.gate(resolved, "plan");
|
|
11536
12068
|
const commitCategory = category;
|
|
12069
|
+
const recipientsPlan = recipientPlan(entry.id, category, request);
|
|
12070
|
+
const facts = await this.policyFacts(resolved, category, recipientsPlan, ctx);
|
|
12071
|
+
const decision = evaluatePolicy(this.runtime.config, entry, category, request, facts);
|
|
12072
|
+
if (decision.effect === "deny") throw policyError(decision, request.account);
|
|
11537
12073
|
const limits = fileLimits(decision);
|
|
11538
12074
|
let dryRunArgv;
|
|
11539
12075
|
let commitArgv;
|
|
@@ -11564,14 +12100,16 @@ var Operations = class {
|
|
|
11564
12100
|
const dry = await this.accounts.run(dryRunArgv, request.account, entry.id, ctx, { scopes: entry.scopes, dryRun: true, cwd: stage?.dir });
|
|
11565
12101
|
report2(ctx, { status: "parsing", operation: entry.id });
|
|
11566
12102
|
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));
|
|
12103
|
+
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11568
12104
|
} finally {
|
|
11569
12105
|
await stage?.dispose();
|
|
11570
12106
|
}
|
|
11571
12107
|
throwIfCancelled(ctx.signal);
|
|
11572
12108
|
const reason = request.reason?.trim();
|
|
11573
|
-
const
|
|
11574
|
-
const
|
|
12109
|
+
const { recipients } = facts;
|
|
12110
|
+
const reached = category === "send" || category === "share" ? { ...preview, recipients: recipientLabels(recipients), ...recipients.unknown ? { recipientsUnknown: recipients.unknown } : {} } : preview;
|
|
12111
|
+
const planPreview = reason ? { ...reached, reason } : reached;
|
|
12112
|
+
const plan2 = this.runtime.plans.create({
|
|
11575
12113
|
account: request.account,
|
|
11576
12114
|
sessionId: ctx.sessionId,
|
|
11577
12115
|
agentId: ctx.agentId,
|
|
@@ -11581,6 +12119,8 @@ var Operations = class {
|
|
|
11581
12119
|
argv: commitArgv,
|
|
11582
12120
|
preview: planPreview,
|
|
11583
12121
|
files: planned,
|
|
12122
|
+
// Recipients read at plan time can change before the commit (a draft, an event's guests): the commit reads them again.
|
|
12123
|
+
...recipientsPlan.lookups.length > 0 ? { recheck: { request, recipients: facts.recipients.addresses.concat(facts.recipients.domains) } } : {},
|
|
11584
12124
|
ttlSeconds: this.runtime.config.policy.planTtlSeconds
|
|
11585
12125
|
});
|
|
11586
12126
|
this.runtime.audit.emit({
|
|
@@ -11594,8 +12134,8 @@ var Operations = class {
|
|
|
11594
12134
|
});
|
|
11595
12135
|
return {
|
|
11596
12136
|
ok: true,
|
|
11597
|
-
planId:
|
|
11598
|
-
expiresAt: new Date(
|
|
12137
|
+
planId: plan2.id,
|
|
12138
|
+
expiresAt: new Date(plan2.expiresAt).toISOString(),
|
|
11599
12139
|
category: commitCategory,
|
|
11600
12140
|
preview: planPreview,
|
|
11601
12141
|
dryRun: redactDryRun(parsed.data)
|
|
@@ -11606,7 +12146,7 @@ var Operations = class {
|
|
|
11606
12146
|
throwIfCancelled(ctx.signal);
|
|
11607
12147
|
if (existing.files?.output) await checkDestination(existing.files.output, existing.files.limits);
|
|
11608
12148
|
this.runtime.assertActive(ctx.signal);
|
|
11609
|
-
const
|
|
12149
|
+
const plan2 = this.runtime.plans.acquire(input.planId, {
|
|
11610
12150
|
sessionId: ctx.sessionId,
|
|
11611
12151
|
agentId: ctx.agentId,
|
|
11612
12152
|
account: existing.account,
|
|
@@ -11616,39 +12156,41 @@ var Operations = class {
|
|
|
11616
12156
|
let stage;
|
|
11617
12157
|
let receipt;
|
|
11618
12158
|
try {
|
|
11619
|
-
if (
|
|
11620
|
-
if (stage
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
12159
|
+
if (plan2.recheck) await this.recheck(plan2, ctx);
|
|
12160
|
+
if (plan2.files) stage = await Stage.create(this.runtime.workDir, ctx.signal);
|
|
12161
|
+
if (stage && plan2.files) {
|
|
12162
|
+
const staged = await stage.stageInputs(plan2.files.inputs, plan2.files.limits);
|
|
12163
|
+
refuseChanged(plan2.files.inputs, staged);
|
|
12164
|
+
if (plan2.files.output) await stage.prepareOutput(plan2.files.output);
|
|
12165
|
+
}
|
|
12166
|
+
const method = commandIndex().methods.get(plan2.operation);
|
|
11626
12167
|
const scopes = method?.scopes;
|
|
11627
|
-
const result = await this.accounts.run(
|
|
11628
|
-
report2(ctx, { status: "parsing", operation:
|
|
12168
|
+
const result = await this.accounts.run(plan2.argv, plan2.account, plan2.operation, ctx, { scopes, cwd: stage?.dir });
|
|
12169
|
+
report2(ctx, { status: "parsing", operation: plan2.operation });
|
|
11629
12170
|
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 &&
|
|
12171
|
+
if (!parsed.ok) throw processError(result, parsed, plan2.service, this.accounts.remediation(plan2.account, scopes, plan2.service));
|
|
12172
|
+
const delivered = stage && plan2.files?.output ? await stage.deliver(plan2.files.output, plan2.files.limits) : void 0;
|
|
11632
12173
|
const file = delivered ? { path: delivered.path, bytes: delivered.bytes, sha256: delivered.sha256 } : void 0;
|
|
11633
|
-
this.runtime.plans.complete(
|
|
12174
|
+
this.runtime.plans.complete(plan2.id);
|
|
11634
12175
|
this.runtime.audit.emit({
|
|
11635
12176
|
event: "operation.commit",
|
|
11636
12177
|
outcome: "success",
|
|
11637
|
-
account:
|
|
12178
|
+
account: plan2.account,
|
|
11638
12179
|
category,
|
|
11639
|
-
operation:
|
|
12180
|
+
operation: plan2.operation,
|
|
11640
12181
|
durationMs: Date.now() - started
|
|
11641
12182
|
});
|
|
11642
|
-
|
|
12183
|
+
const answer = plan2.files?.output ? parsed.data : withoutSavedBody(parsed.data);
|
|
12184
|
+
receipt = { ok: true, planId: plan2.id, category, operation: plan2.operation, result: answer ?? parsed.raw, file, ...delivered?.cleanupDegraded ? { cleanupDegraded: true } : {} };
|
|
11643
12185
|
return receipt;
|
|
11644
12186
|
} catch (error2) {
|
|
11645
|
-
this.runtime.plans.release(
|
|
12187
|
+
this.runtime.plans.release(plan2.id);
|
|
11646
12188
|
this.runtime.audit.emit({
|
|
11647
12189
|
event: "operation.commit",
|
|
11648
12190
|
outcome: "error",
|
|
11649
|
-
account:
|
|
12191
|
+
account: plan2.account,
|
|
11650
12192
|
category,
|
|
11651
|
-
operation:
|
|
12193
|
+
operation: plan2.operation,
|
|
11652
12194
|
durationMs: Date.now() - started,
|
|
11653
12195
|
errorCode: auditErrorCode(error2)
|
|
11654
12196
|
});
|
|
@@ -11668,6 +12210,85 @@ var Operations = class {
|
|
|
11668
12210
|
}
|
|
11669
12211
|
}
|
|
11670
12212
|
}
|
|
12213
|
+
/**
|
|
12214
|
+
* The recipient facts for a plan: what the request carries, the reads
|
|
12215
|
+
* that find the rest, and the account's own domains when a rule needs them.
|
|
12216
|
+
* A read that fails leaves the recipients unknown, which a recipient rule refuses.
|
|
12217
|
+
*/
|
|
12218
|
+
async policyFacts(resolved, category, plan2, ctx) {
|
|
12219
|
+
const { entry, request } = resolved;
|
|
12220
|
+
let recipients = plan2.known;
|
|
12221
|
+
if (plan2.lookups.length > 0) {
|
|
12222
|
+
const self = plan2.needsSelf ? await this.ownAddress(request.account, entry.service, ctx) : void 0;
|
|
12223
|
+
for (const lookup of plan2.lookups) recipients = withLookup(recipients, await this.lookup(lookup, request.account, ctx, self), lookup.what);
|
|
12224
|
+
}
|
|
12225
|
+
const configured = this.runtime.config.accounts[request.account]?.domains;
|
|
12226
|
+
const reaches = recipients.addresses.length > 0 || recipients.domains.length > 0 || recipients.unknown !== void 0;
|
|
12227
|
+
if (configured || !reaches || !needsAccountDomains(this.runtime.config, entry, category, request)) return { recipients, accountDomains: configured };
|
|
12228
|
+
const address = await this.ownAddress(request.account, entry.service, ctx);
|
|
12229
|
+
return { recipients, accountDomains: address ? [domainOf(address)] : void 0 };
|
|
12230
|
+
}
|
|
12231
|
+
/** Read the recipients again before a commit whose plan read them, and refuse when the policy now denies or someone was added. */
|
|
12232
|
+
async recheck(plan2, ctx) {
|
|
12233
|
+
const recheck = plan2.recheck;
|
|
12234
|
+
const resolved = this.resolve(recheck.request);
|
|
12235
|
+
const category = plan2.category;
|
|
12236
|
+
const facts = await this.policyFacts(resolved, category, recipientPlan(resolved.entry.id, category, resolved.request), ctx);
|
|
12237
|
+
const decision = evaluatePolicy(this.runtime.config, resolved.entry, category, resolved.request, facts);
|
|
12238
|
+
if (decision.effect === "deny") throw policyError(decision, plan2.account);
|
|
12239
|
+
const planned = new Set(recheck.recipients);
|
|
12240
|
+
const added = [...facts.recipients.addresses, ...facts.recipients.domains].filter((recipient) => !planned.has(recipient));
|
|
12241
|
+
if (added.length > 0) {
|
|
12242
|
+
throw new GwsError("PLAN_EXPIRED", `the recipients changed after the plan was created (added: ${added.join(", ")})`, { next: "Plan again and show the new preview." });
|
|
12243
|
+
}
|
|
12244
|
+
}
|
|
12245
|
+
/** One recipient read; undefined when it fails or answers in an unexpected shape. */
|
|
12246
|
+
async lookup(lookup, account2, ctx, self) {
|
|
12247
|
+
try {
|
|
12248
|
+
return lookup.extract(await this.readJson(lookup.operation, lookup.params, account2, ctx), self);
|
|
12249
|
+
} catch (error2) {
|
|
12250
|
+
throwIfCancelled(ctx.signal);
|
|
12251
|
+
this.runtime.assertActive(ctx.signal);
|
|
12252
|
+
if (GwsError.from(error2) === void 0) throw error2;
|
|
12253
|
+
return void 0;
|
|
12254
|
+
}
|
|
12255
|
+
}
|
|
12256
|
+
/** The account's own address, read with the operation's service first; undefined when no read gives it. */
|
|
12257
|
+
async ownAddress(account2, service, ctx) {
|
|
12258
|
+
const cached3 = this.addresses.get(account2);
|
|
12259
|
+
if (cached3 && Date.now() - cached3.at < ADDRESS_TTL_MS) return cached3.address;
|
|
12260
|
+
const enabled = this.runtime.config.accounts[account2]?.enabledServices ?? [];
|
|
12261
|
+
const services = [.../* @__PURE__ */ new Set([service, ...Object.keys(ACCOUNT_ADDRESS_READS)])].filter((name) => Object.hasOwn(ACCOUNT_ADDRESS_READS, name) && enabled.includes(name));
|
|
12262
|
+
for (const name of services) {
|
|
12263
|
+
const read = ACCOUNT_ADDRESS_READS[name];
|
|
12264
|
+
try {
|
|
12265
|
+
const address = read.extract(await this.readJson(read.operation, read.params, account2, ctx));
|
|
12266
|
+
if (address) {
|
|
12267
|
+
this.addresses.set(account2, { address, at: Date.now() });
|
|
12268
|
+
return address;
|
|
12269
|
+
}
|
|
12270
|
+
} catch (error2) {
|
|
12271
|
+
throwIfCancelled(ctx.signal);
|
|
12272
|
+
this.runtime.assertActive(ctx.signal);
|
|
12273
|
+
if (GwsError.from(error2) === void 0) throw error2;
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
return void 0;
|
|
12277
|
+
}
|
|
12278
|
+
/** A field-masked read the plugin makes for itself, under the read policy; its parsed JSON. */
|
|
12279
|
+
async readJson(operation, params, account2, ctx) {
|
|
12280
|
+
const entry = commandIndex().methods.get(operation);
|
|
12281
|
+
if (!entry) throw new GwsError("OPERATION_UNKNOWN", `'${operation}' is not in the gws manifest`);
|
|
12282
|
+
const request = { account: account2, operation, params };
|
|
12283
|
+
const decision = evaluatePolicy(this.runtime.config, entry, "read", request, { recipients: recipientPlan(operation, "read", request).known });
|
|
12284
|
+
if (decision.effect === "deny") throw policyError(decision, account2);
|
|
12285
|
+
const argv = buildCommand(entry, request, this.runtime.config, { dryRun: false, category: "read" }).argv;
|
|
12286
|
+
const result = await this.accounts.run(argv, account2, operation, ctx, { scopes: entry.scopes });
|
|
12287
|
+
this.runtime.assertActive(ctx.signal);
|
|
12288
|
+
const parsed = parseResult(result, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
12289
|
+
if (!parsed.ok) throw processError(result, parsed, entry.service, this.accounts.remediation(account2, entry.scopes, entry.service));
|
|
12290
|
+
return parsed.data;
|
|
12291
|
+
}
|
|
11671
12292
|
};
|
|
11672
12293
|
function fileLimits(decision) {
|
|
11673
12294
|
const { maxUploadBytes, maxDownloadBytes, readRoots, writeRoots } = decision.constraints;
|
|
@@ -11702,10 +12323,9 @@ var Runtime = class _Runtime {
|
|
|
11702
12323
|
this.version = version2;
|
|
11703
12324
|
this.binary = binary;
|
|
11704
12325
|
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);
|
|
12326
|
+
this.workDir = createWorkDir();
|
|
12327
|
+
this.audit = new Audit({ ...config2.audit, path: config2.audit.path ?? join9(this.paths.stateDir, "audit.jsonl") }, options.pluginVersion);
|
|
12328
|
+
this.schema = new SchemaClient(binary, this.workDir, localEnv(this.workDir, ownedDir(join9(this.paths.cacheDir, "discovery"))), config2.execution.schemaTimeoutMs);
|
|
11709
12329
|
this.accounts = new Accounts(this);
|
|
11710
12330
|
this.operations = new Operations(this, this.accounts);
|
|
11711
12331
|
}
|
|
@@ -11743,7 +12363,8 @@ var Runtime = class _Runtime {
|
|
|
11743
12363
|
}
|
|
11744
12364
|
static async create(config2, options) {
|
|
11745
12365
|
const binary = resolveBinary(config2, options.projectDirectory);
|
|
11746
|
-
|
|
12366
|
+
removeStaleWorkDirs();
|
|
12367
|
+
const probeDir = mkdtempSync2(join9(tmpdir2(), "opencode-gws-probe-"));
|
|
11747
12368
|
let version2 = "unknown";
|
|
11748
12369
|
try {
|
|
11749
12370
|
const probe = await runProcess({
|
|
@@ -11760,7 +12381,7 @@ var Runtime = class _Runtime {
|
|
|
11760
12381
|
throw new Error(`GWS_VERSION_MISMATCH: Requires ${GWS_VERSION} and the matching manifest`);
|
|
11761
12382
|
}
|
|
11762
12383
|
} finally {
|
|
11763
|
-
|
|
12384
|
+
rmSync2(probeDir, { recursive: true, force: true });
|
|
11764
12385
|
}
|
|
11765
12386
|
return new _Runtime(config2, options, binary, version2);
|
|
11766
12387
|
}
|
|
@@ -11824,7 +12445,7 @@ var Runtime = class _Runtime {
|
|
|
11824
12445
|
this.plans.invalidateAll();
|
|
11825
12446
|
this.audit.close();
|
|
11826
12447
|
this.closing = settled.then(() => {
|
|
11827
|
-
|
|
12448
|
+
rmSync2(this.workDir, { recursive: true, force: true });
|
|
11828
12449
|
});
|
|
11829
12450
|
return this.closing;
|
|
11830
12451
|
}
|
|
@@ -11847,8 +12468,7 @@ async function start(ctx) {
|
|
|
11847
12468
|
const ids = integrationIds(config2.accounts);
|
|
11848
12469
|
const runtime = await Runtime.create(config2, {
|
|
11849
12470
|
pluginVersion: PLUGIN_VERSION,
|
|
11850
|
-
|
|
11851
|
-
hostWarning: host.warning,
|
|
12471
|
+
host,
|
|
11852
12472
|
projectDirectory: ctx.location.directory,
|
|
11853
12473
|
tokenProvider: integrationTokenProvider(ctx, config2, ids),
|
|
11854
12474
|
connection: integrationConnection(ctx, ids),
|