@joe-broadhead/opencode-gws 0.0.1 → 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 +36 -0
- package/README.md +28 -9
- package/dist/index.js +1237 -512
- package/dist/tui.js +34 -10
- package/package.json +7 -6
- package/skills/opencode-gws/SKILL.md +5 -2
- package/skills/opencode-gws-admin/SKILL.md +1 -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: {
|
|
@@ -6618,6 +6626,10 @@ var ErrorCode = {
|
|
|
6618
6626
|
meaning: "The sign-in has not granted a scope this operation needs. The plugin checks before gws runs, so nothing ran.",
|
|
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
|
},
|
|
6629
|
+
ADMIN_PRIVILEGE_REQUIRED: {
|
|
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.",
|
|
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."
|
|
6632
|
+
},
|
|
6621
6633
|
AUTH_CLIENT_REQUIRED: {
|
|
6622
6634
|
meaning: "No OAuth client is configured for the sign-in, none was entered in the form, and no earlier sign-in has one.",
|
|
6623
6635
|
next: "Run /connect and choose Google Workspace, then enter your Desktop OAuth client ID and secret, or ask your administrator."
|
|
@@ -6702,8 +6714,8 @@ var ErrorCode = {
|
|
|
6702
6714
|
},
|
|
6703
6715
|
// ── Setup: these stop the plugin from loading ────────────────────────
|
|
6704
6716
|
HOST_UNTESTED: {
|
|
6705
|
-
meaning: "The OpenCode version is outside the
|
|
6706
|
-
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."
|
|
6707
6719
|
},
|
|
6708
6720
|
OPTIONS_INVALID: {
|
|
6709
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.",
|
|
@@ -6768,8 +6780,8 @@ var GwsError = class _GwsError extends Error {
|
|
|
6768
6780
|
return stated ? new _GwsError(code2, stated[1], { next: stated[2] }) : new _GwsError(code2, match[2]);
|
|
6769
6781
|
}
|
|
6770
6782
|
};
|
|
6771
|
-
function sentence(
|
|
6772
|
-
const trimmed =
|
|
6783
|
+
function sentence(text4) {
|
|
6784
|
+
const trimmed = text4.trim();
|
|
6773
6785
|
const capital = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
6774
6786
|
return /[.!?]$/.test(capital) ? capital : `${capital}.`;
|
|
6775
6787
|
}
|
|
@@ -6785,11 +6797,11 @@ import { readFileSync } from "node:fs";
|
|
|
6785
6797
|
import { dirname, join } from "node:path";
|
|
6786
6798
|
import { fileURLToPath } from "node:url";
|
|
6787
6799
|
|
|
6788
|
-
// src/domain/
|
|
6789
|
-
var
|
|
6790
|
-
|
|
6791
|
-
);
|
|
6792
|
-
var
|
|
6800
|
+
// src/domain/restricted.ts
|
|
6801
|
+
var PREFIX = "https://www.googleapis.com/auth/";
|
|
6802
|
+
var APP_ONLY_SCOPES = ["chat.bot", "chat.import", "classroom.addons.student", "classroom.addons.teacher"].map((name) => `${PREFIX}${name}`);
|
|
6803
|
+
var DELEGATION_SCOPES = ["keep", "keep.readonly"].map((name) => `${PREFIX}${name}`);
|
|
6804
|
+
var APP_ONLY_METHODS = [
|
|
6793
6805
|
// chat: a Chat app's own credentials
|
|
6794
6806
|
"chat.spaces.completeImport",
|
|
6795
6807
|
"chat.spaces.messages.attachments.get",
|
|
@@ -6822,9 +6834,33 @@ var USER_UNUSABLE_METHODS = [
|
|
|
6822
6834
|
"classroom.courses.posts.addOnAttachments.studentSubmissions.patch",
|
|
6823
6835
|
"classroom.courses.posts.getAddOnContext"
|
|
6824
6836
|
];
|
|
6825
|
-
var
|
|
6826
|
-
|
|
6827
|
-
|
|
6837
|
+
var DELEGATION_METHODS = [
|
|
6838
|
+
"keep.media.download",
|
|
6839
|
+
"keep.notes.create",
|
|
6840
|
+
"keep.notes.delete",
|
|
6841
|
+
"keep.notes.get",
|
|
6842
|
+
"keep.notes.list",
|
|
6843
|
+
"keep.notes.permissions.batchCreate",
|
|
6844
|
+
"keep.notes.permissions.batchDelete"
|
|
6845
|
+
];
|
|
6846
|
+
var DELEGATION_SERVICES = ["keep"];
|
|
6847
|
+
var RESTRICTIONS = new Map([
|
|
6848
|
+
...APP_ONLY_METHODS.map((id) => [id, "app-only"]),
|
|
6849
|
+
...DELEGATION_METHODS.map((id) => [id, "domain-wide-delegation"])
|
|
6850
|
+
]);
|
|
6851
|
+
function restriction(operation) {
|
|
6852
|
+
return RESTRICTIONS.get(operation);
|
|
6853
|
+
}
|
|
6854
|
+
function addsDelegationScope(scopes) {
|
|
6855
|
+
return scopes.some((scope) => DELEGATION_SCOPES.includes(scope));
|
|
6856
|
+
}
|
|
6857
|
+
function delegationRequired(services, scopes) {
|
|
6858
|
+
return addsDelegationScope(scopes) ? [] : services.filter((service) => DELEGATION_SERVICES.includes(service));
|
|
6859
|
+
}
|
|
6860
|
+
function restrictionMarks(operation) {
|
|
6861
|
+
const reason = restriction(operation);
|
|
6862
|
+
if (!reason) return {};
|
|
6863
|
+
return reason === "app-only" ? { appOnly: true, restriction: reason } : { restriction: reason };
|
|
6828
6864
|
}
|
|
6829
6865
|
|
|
6830
6866
|
// src/domain/helpers.ts
|
|
@@ -6945,22 +6981,29 @@ var replyBase = {
|
|
|
6945
6981
|
messageId: string2().min(1),
|
|
6946
6982
|
body: string2().min(1).max(1e5),
|
|
6947
6983
|
from: string2().email().optional(),
|
|
6984
|
+
to: union([string2(), array(string2())]).optional().describe("Extra To recipients, besides the ones the reply goes to"),
|
|
6948
6985
|
cc: union([string2(), array(string2())]).optional(),
|
|
6949
6986
|
bcc: union([string2(), array(string2())]).optional(),
|
|
6950
6987
|
html: boolean2().optional(),
|
|
6951
6988
|
draft: boolean2().optional(),
|
|
6952
6989
|
attachments: array(absolutePath).max(20).optional()
|
|
6953
6990
|
};
|
|
6954
|
-
|
|
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) {
|
|
6955
6996
|
return {
|
|
6956
6997
|
id,
|
|
6957
6998
|
service: "gmail",
|
|
6958
6999
|
category: "send",
|
|
6959
|
-
inputSchema: strictObject(replyBase),
|
|
7000
|
+
inputSchema: strictObject(all ? replyAllBase : replyBase),
|
|
6960
7001
|
build(input, request) {
|
|
6961
7002
|
const parsed = this.inputSchema.parse(input);
|
|
6962
7003
|
const argv = ["gmail", name, "--message-id", parsed.messageId, "--body", parsed.body];
|
|
6963
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(","));
|
|
6964
7007
|
if (parsed.cc) argv.push("--cc", emails(parsed.cc).join(","));
|
|
6965
7008
|
if (parsed.bcc) argv.push("--bcc", emails(parsed.bcc).join(","));
|
|
6966
7009
|
if (parsed.html) argv.push("--html");
|
|
@@ -6973,9 +7016,9 @@ function replyAdapter(id, name) {
|
|
|
6973
7016
|
files,
|
|
6974
7017
|
preview: {
|
|
6975
7018
|
method: `gmail ${name}`,
|
|
6976
|
-
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 },
|
|
6977
7020
|
bodySummary: bytes(parsed.body),
|
|
6978
|
-
recipients: [...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
7021
|
+
recipients: [...emails(parsed.to), ...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
6979
7022
|
uploads: parsed.attachments,
|
|
6980
7023
|
account: request.account,
|
|
6981
7024
|
category: "send"
|
|
@@ -6984,30 +7027,45 @@ function replyAdapter(id, name) {
|
|
|
6984
7027
|
}
|
|
6985
7028
|
};
|
|
6986
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
|
+
});
|
|
6987
7042
|
var gmailForward = {
|
|
6988
7043
|
id: "gmail.forward",
|
|
6989
7044
|
service: "gmail",
|
|
6990
7045
|
category: "send",
|
|
6991
|
-
inputSchema:
|
|
6992
|
-
messageId: string2().min(1),
|
|
6993
|
-
to: union([string2(), array(string2())]),
|
|
6994
|
-
body: string2().max(1e5).optional(),
|
|
6995
|
-
html: boolean2().optional()
|
|
6996
|
-
}),
|
|
7046
|
+
inputSchema: forwardSchema,
|
|
6997
7047
|
build(input, request) {
|
|
6998
7048
|
const parsed = this.inputSchema.parse(input);
|
|
6999
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);
|
|
7000
7053
|
if (parsed.body) argv.push("--body", parsed.body);
|
|
7001
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);
|
|
7002
7058
|
return {
|
|
7003
7059
|
id: this.id,
|
|
7004
7060
|
category: "send",
|
|
7005
7061
|
argv,
|
|
7062
|
+
files,
|
|
7006
7063
|
preview: {
|
|
7007
7064
|
method: "gmail +forward",
|
|
7008
|
-
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 },
|
|
7009
7066
|
bodySummary: parsed.body ? bytes(parsed.body) : void 0,
|
|
7010
|
-
recipients: emails(parsed.to),
|
|
7067
|
+
recipients: [...emails(parsed.to), ...emails(parsed.cc), ...emails(parsed.bcc)],
|
|
7068
|
+
uploads: parsed.attachments,
|
|
7011
7069
|
account: request.account,
|
|
7012
7070
|
category: "send"
|
|
7013
7071
|
}
|
|
@@ -7021,7 +7079,8 @@ var gmailTriage = {
|
|
|
7021
7079
|
inputSchema: strictObject({
|
|
7022
7080
|
max: number2().int().positive().max(200).optional(),
|
|
7023
7081
|
query: string2().max(2e3).optional(),
|
|
7024
|
-
labels: boolean2().optional()
|
|
7082
|
+
labels: boolean2().optional(),
|
|
7083
|
+
format: outputFormat.optional()
|
|
7025
7084
|
}),
|
|
7026
7085
|
build(input, request) {
|
|
7027
7086
|
const parsed = this.inputSchema.parse(input);
|
|
@@ -7029,6 +7088,7 @@ var gmailTriage = {
|
|
|
7029
7088
|
if (parsed.max) argv.push("--max", String(parsed.max));
|
|
7030
7089
|
if (parsed.query) argv.push("--query", parsed.query);
|
|
7031
7090
|
if (parsed.labels) argv.push("--labels");
|
|
7091
|
+
if (parsed.format) argv.push("--format", parsed.format);
|
|
7032
7092
|
return {
|
|
7033
7093
|
id: this.id,
|
|
7034
7094
|
category: "read",
|
|
@@ -7189,8 +7249,8 @@ var weeklyDigest = {
|
|
|
7189
7249
|
var ADAPTERS = /* @__PURE__ */ new Map([
|
|
7190
7250
|
[gmailSend.id, gmailSend],
|
|
7191
7251
|
[gmailRead.id, gmailRead],
|
|
7192
|
-
["gmail.reply", replyAdapter("gmail.reply", "+reply")],
|
|
7193
|
-
["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)],
|
|
7194
7254
|
[gmailForward.id, gmailForward],
|
|
7195
7255
|
[gmailTriage.id, gmailTriage],
|
|
7196
7256
|
[calendarAgenda.id, calendarAgenda],
|
|
@@ -7290,9 +7350,9 @@ function commitTool(category) {
|
|
|
7290
7350
|
function words(query) {
|
|
7291
7351
|
return (query ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
7292
7352
|
}
|
|
7293
|
-
function startsWord(
|
|
7294
|
-
for (let index2 =
|
|
7295
|
-
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;
|
|
7296
7356
|
}
|
|
7297
7357
|
return false;
|
|
7298
7358
|
}
|
|
@@ -7350,7 +7410,7 @@ function catalog(input = {}, services) {
|
|
|
7350
7410
|
...method.pathTemplate ? { path: method.pathTemplate } : {},
|
|
7351
7411
|
executable: methodExecutable(method),
|
|
7352
7412
|
...tool && methodExecutable(method) ? { commitTool: tool } : {},
|
|
7353
|
-
...
|
|
7413
|
+
...restrictionMarks(method.id)
|
|
7354
7414
|
});
|
|
7355
7415
|
}
|
|
7356
7416
|
}
|
|
@@ -7513,7 +7573,8 @@ var DIRECT = {
|
|
|
7513
7573
|
forms: { read: ["forms.body.readonly", "forms.responses.readonly"], write: ["forms.body", "forms.responses.readonly", "drive.file"] },
|
|
7514
7574
|
// meetings.space.created reaches only spaces this client created; write keeps reading the others.
|
|
7515
7575
|
meet: { read: ["meetings.space.readonly"], write: ["meetings.space.created", "meetings.space.readonly"] },
|
|
7516
|
-
|
|
7576
|
+
// Google grants Keep's scopes only through domain-wide delegation (restricted.ts), so neither level requests them.
|
|
7577
|
+
keep: { read: [], write: [] },
|
|
7517
7578
|
// Reports are read-only at both levels.
|
|
7518
7579
|
"admin-reports": {
|
|
7519
7580
|
read: ["admin.reports.audit.readonly", "admin.reports.usage.readonly"],
|
|
@@ -7601,15 +7662,18 @@ var ClientJsonSchema = union([
|
|
|
7601
7662
|
var OAuthClientSchema = union([DesktopClientSchema, ClientJsonSchema]);
|
|
7602
7663
|
var accessLevel = _enum(["read", "write"]);
|
|
7603
7664
|
function defaultAccounts() {
|
|
7604
|
-
|
|
7665
|
+
const services = loadManifest().services.map((service) => service.alias).filter((service) => !DELEGATION_SERVICES.includes(service));
|
|
7666
|
+
return { default: { mode: "interactive", enabledServices: services, access: "write" } };
|
|
7605
7667
|
}
|
|
7606
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);
|
|
7607
7670
|
var accountProfile = preprocess(
|
|
7608
7671
|
(value) => value !== null && typeof value === "object" && !Array.isArray(value) && !("mode" in value) ? { ...value, mode: "interactive" } : value,
|
|
7609
7672
|
discriminatedUnion("mode", [
|
|
7610
7673
|
strictObject({
|
|
7611
7674
|
mode: literal("interactive"),
|
|
7612
7675
|
enabledServices,
|
|
7676
|
+
domains: domains.optional(),
|
|
7613
7677
|
access: union([accessLevel, record(string2().min(1), accessLevel)]).optional(),
|
|
7614
7678
|
scopes: array(string2().min(1)).optional(),
|
|
7615
7679
|
oauthClient: OAuthClientSchema.optional()
|
|
@@ -7617,11 +7681,13 @@ var accountProfile = preprocess(
|
|
|
7617
7681
|
strictObject({
|
|
7618
7682
|
mode: literal("credentials_file"),
|
|
7619
7683
|
enabledServices,
|
|
7684
|
+
domains: domains.optional(),
|
|
7620
7685
|
credentialsFileEnv: envVarName
|
|
7621
7686
|
}),
|
|
7622
7687
|
strictObject({
|
|
7623
7688
|
mode: literal("access_token"),
|
|
7624
7689
|
enabledServices,
|
|
7690
|
+
domains: domains.optional(),
|
|
7625
7691
|
tokenEnv: envVarName
|
|
7626
7692
|
})
|
|
7627
7693
|
])
|
|
@@ -7745,6 +7811,7 @@ function loadConfig(raw) {
|
|
|
7745
7811
|
accounts[alias] = {
|
|
7746
7812
|
mode: profile.mode,
|
|
7747
7813
|
enabledServices: enabled,
|
|
7814
|
+
...profile.domains ? { domains: [...new Set(profile.domains.map((domain) => domain.toLowerCase()))] } : {},
|
|
7748
7815
|
access,
|
|
7749
7816
|
...accessSpecified ? { accessSpecified } : {},
|
|
7750
7817
|
scopes: [...new Set(scopes)],
|
|
@@ -7811,10 +7878,9 @@ function loadConfig(raw) {
|
|
|
7811
7878
|
};
|
|
7812
7879
|
}
|
|
7813
7880
|
|
|
7814
|
-
// src/domain/
|
|
7815
|
-
var PLUGIN_VERSION = "0.0.1";
|
|
7816
|
-
var PLUGIN_ID = "google-workspace.gws";
|
|
7881
|
+
// src/domain/host.ts
|
|
7817
7882
|
var HOST_RANGE = ">=2.2.3 <2.3.0";
|
|
7883
|
+
var TESTED_HOSTS = ["2.2.3", "2.2.4", "2.2.5"];
|
|
7818
7884
|
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
7819
7885
|
var RANGE = /^>=(\d+)\.(\d+)\.(\d+) <(\d+)\.(\d+)\.(\d+)$/;
|
|
7820
7886
|
function compare(a, b) {
|
|
@@ -7833,18 +7899,20 @@ function satisfies(version2, range) {
|
|
|
7833
7899
|
return compare(triple, floor) >= 0 && compare(triple, ceiling) < 0;
|
|
7834
7900
|
}
|
|
7835
7901
|
function checkHost(version2, range, allowUntested) {
|
|
7836
|
-
const
|
|
7837
|
-
if (satisfies(
|
|
7838
|
-
const problem = `tested with OpenCode ${range}, found ${text3}`;
|
|
7839
|
-
if (!allowUntested) {
|
|
7902
|
+
const text4 = typeof version2 === "string" && version2.trim() ? version2.trim() : void 0;
|
|
7903
|
+
if (!(text4 && satisfies(text4, range)) && !allowUntested) {
|
|
7840
7904
|
const [major, minor, patch] = bounds(range).floor;
|
|
7841
7905
|
throw new Error(
|
|
7842
|
-
`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.`
|
|
7843
7907
|
);
|
|
7844
7908
|
}
|
|
7845
|
-
return { version:
|
|
7909
|
+
return { version: text4 ?? null, range, tested: text4 !== void 0 && TESTED_HOSTS.includes(text4.replace(/^v/, "")) };
|
|
7846
7910
|
}
|
|
7847
7911
|
|
|
7912
|
+
// src/domain/version.ts
|
|
7913
|
+
var PLUGIN_VERSION = "0.0.3";
|
|
7914
|
+
var PLUGIN_ID = "google-workspace.gws";
|
|
7915
|
+
|
|
7848
7916
|
// src/host/agents.ts
|
|
7849
7917
|
var ASK_ACTIONS = ["gws.write", "gws.export", "gws.send", "gws.share", "gws.delete", "gws.admin"];
|
|
7850
7918
|
var ASK_RULES = ASK_ACTIONS.map((action) => ({ action, resource: "*", effect: "ask" }));
|
|
@@ -7920,6 +7988,7 @@ var object2 = (properties, required2) => ({ type: "object", properties, required
|
|
|
7920
7988
|
var MODES = ["interactive", "credentials_file", "access_token"];
|
|
7921
7989
|
var ACCESS = ["read", "write"];
|
|
7922
7990
|
var CLIENT_SOURCES = ["account", "plugin", "pasted"];
|
|
7991
|
+
var STATES = ["connected", "sign_in_required", "not_configured", "host_untested"];
|
|
7923
7992
|
var account = object2(
|
|
7924
7993
|
{
|
|
7925
7994
|
alias: text,
|
|
@@ -7932,15 +8001,16 @@ var account = object2(
|
|
|
7932
8001
|
clientSource: { enum: CLIENT_SOURCES },
|
|
7933
8002
|
grantedScopes: texts,
|
|
7934
8003
|
missingScopes: texts,
|
|
8004
|
+
delegationRequired: texts,
|
|
7935
8005
|
nextStep: text
|
|
7936
8006
|
},
|
|
7937
8007
|
["alias", "mode", "services", "connected"]
|
|
7938
8008
|
);
|
|
7939
8009
|
var status = object2(
|
|
7940
8010
|
{
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
8011
|
+
version: text,
|
|
8012
|
+
host: object2({ version: { type: ["string", "null"] }, range: text, tested: { type: "boolean" } }, ["version", "range", "tested"]),
|
|
8013
|
+
state: { enum: STATES },
|
|
7944
8014
|
gws: object2({ path: text, version: text }, ["path", "version"]),
|
|
7945
8015
|
services: texts,
|
|
7946
8016
|
accounts: { type: "array", items: account },
|
|
@@ -7949,7 +8019,7 @@ var status = object2(
|
|
|
7949
8019
|
permissionsBlock: text,
|
|
7950
8020
|
nextStep: text
|
|
7951
8021
|
},
|
|
7952
|
-
["
|
|
8022
|
+
["version", "host", "state", "gws", "services", "accounts", "unguardedAgents", "nextStep"]
|
|
7953
8023
|
);
|
|
7954
8024
|
var Gws = Rpc.define({
|
|
7955
8025
|
id: RPC_ID,
|
|
@@ -7967,7 +8037,13 @@ function connectionLabel(account2) {
|
|
|
7967
8037
|
return account2.mode === "interactive" ? "sign-in required" : "not configured";
|
|
7968
8038
|
}
|
|
7969
8039
|
function openCodeLabel(status2) {
|
|
7970
|
-
|
|
8040
|
+
const version2 = status2.host.version ?? "unknown";
|
|
8041
|
+
return status2.host.tested ? `${version2} (tested)` : `${version2} (host untested; supported range ${status2.host.range})`;
|
|
8042
|
+
}
|
|
8043
|
+
var DELEGATION_LABEL = "Needs domain-wide delegation";
|
|
8044
|
+
function delegationText(account2) {
|
|
8045
|
+
const services = account2.delegationRequired ?? [];
|
|
8046
|
+
return `${services.join(", ")} (a Workspace admin sets it up; then add the scopes to accounts.${account2.alias}.scopes)`;
|
|
7971
8047
|
}
|
|
7972
8048
|
var BLOCK_PLACEMENT = "Put this block first in that agent's own permissions, followed by your top-level rules for gws actions. Never put it in the top-level permissions: that would loosen Explore.";
|
|
7973
8049
|
|
|
@@ -8064,10 +8140,11 @@ var Lifecycle = class {
|
|
|
8064
8140
|
// src/domain/plans.ts
|
|
8065
8141
|
import { randomBytes } from "node:crypto";
|
|
8066
8142
|
var MAX_PLANS = 512;
|
|
8067
|
-
function redactedPreview(
|
|
8068
|
-
const { method, recipients, destination, bodySummary, reason } =
|
|
8143
|
+
function redactedPreview(plan2) {
|
|
8144
|
+
const { method, recipients, recipientsUnknown, destination, bodySummary, reason } = plan2.preview;
|
|
8069
8145
|
const view = { method };
|
|
8070
8146
|
if (recipients && recipients.length > 0) view.recipients = [...recipients];
|
|
8147
|
+
if (recipientsUnknown !== void 0) view.recipientsUnknown = recipientsUnknown;
|
|
8071
8148
|
if (destination !== void 0) view.destination = destination;
|
|
8072
8149
|
if (bodySummary !== void 0) view.bodySummary = bodySummary;
|
|
8073
8150
|
if (reason !== void 0) view.reason = reason;
|
|
@@ -8079,14 +8156,14 @@ var PlanStore = class {
|
|
|
8079
8156
|
this.sweep();
|
|
8080
8157
|
const now = Date.now();
|
|
8081
8158
|
const { ttlSeconds, ...fields } = input;
|
|
8082
|
-
const
|
|
8159
|
+
const plan2 = {
|
|
8083
8160
|
...fields,
|
|
8084
8161
|
id: `plan_${randomBytes(18).toString("base64url")}`,
|
|
8085
8162
|
state: "ready",
|
|
8086
8163
|
createdAt: now,
|
|
8087
8164
|
expiresAt: now + ttlSeconds * 1e3
|
|
8088
8165
|
};
|
|
8089
|
-
this.plans.set(
|
|
8166
|
+
this.plans.set(plan2.id, plan2);
|
|
8090
8167
|
while (this.plans.size > MAX_PLANS) {
|
|
8091
8168
|
const oldest = this.plans.keys().next().value;
|
|
8092
8169
|
if (oldest === void 0) break;
|
|
@@ -8094,90 +8171,300 @@ var PlanStore = class {
|
|
|
8094
8171
|
if (evicted) this.expire(evicted);
|
|
8095
8172
|
this.plans.delete(oldest);
|
|
8096
8173
|
}
|
|
8097
|
-
return
|
|
8174
|
+
return plan2;
|
|
8098
8175
|
}
|
|
8099
8176
|
get(id) {
|
|
8100
|
-
const
|
|
8101
|
-
if (!
|
|
8102
|
-
if (
|
|
8103
|
-
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;
|
|
8104
8181
|
}
|
|
8105
8182
|
/** Ready (committable) plans, oldest first, optionally for one session. */
|
|
8106
8183
|
list(filter = {}) {
|
|
8107
8184
|
const ready = [];
|
|
8108
8185
|
for (const id of [...this.plans.keys()]) {
|
|
8109
|
-
const
|
|
8110
|
-
if (
|
|
8111
|
-
if (filter.sessionId !== void 0 &&
|
|
8112
|
-
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);
|
|
8113
8190
|
}
|
|
8114
8191
|
return ready.sort((a, b) => a.createdAt - b.createdAt);
|
|
8115
8192
|
}
|
|
8116
8193
|
/** Validate a commit binding without consuming the plan. */
|
|
8117
8194
|
check(id, binding) {
|
|
8118
|
-
const
|
|
8119
|
-
if (!
|
|
8120
|
-
if (
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
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) {
|
|
8124
8203
|
throw new GwsError("PLAN_CALLER_MISMATCH", "a plan is bound to the session and agent that created it");
|
|
8125
8204
|
}
|
|
8126
|
-
if (
|
|
8205
|
+
if (plan2.category !== binding.category) {
|
|
8127
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}." });
|
|
8128
8207
|
}
|
|
8129
|
-
return
|
|
8208
|
+
return plan2;
|
|
8130
8209
|
}
|
|
8131
8210
|
acquire(id, binding) {
|
|
8132
|
-
const
|
|
8133
|
-
if (
|
|
8211
|
+
const plan2 = this.check(id, binding);
|
|
8212
|
+
if (plan2.account !== binding.account) {
|
|
8134
8213
|
throw new GwsError("PLAN_CALLER_MISMATCH", "a plan is bound to the account that created it");
|
|
8135
8214
|
}
|
|
8136
|
-
|
|
8137
|
-
return
|
|
8215
|
+
plan2.state = "committing";
|
|
8216
|
+
return plan2;
|
|
8138
8217
|
}
|
|
8139
8218
|
complete(id) {
|
|
8140
|
-
const
|
|
8141
|
-
if (
|
|
8142
|
-
|
|
8219
|
+
const plan2 = this.plans.get(id);
|
|
8220
|
+
if (plan2?.state !== "committing") return;
|
|
8221
|
+
plan2.state = "consumed";
|
|
8143
8222
|
}
|
|
8144
8223
|
release(id) {
|
|
8145
|
-
const
|
|
8146
|
-
if (
|
|
8224
|
+
const plan2 = this.plans.get(id);
|
|
8225
|
+
if (plan2 && plan2.state === "committing") this.expire(plan2);
|
|
8147
8226
|
}
|
|
8148
8227
|
invalidateAll() {
|
|
8149
|
-
for (const
|
|
8228
|
+
for (const plan2 of this.plans.values()) this.expire(plan2);
|
|
8150
8229
|
}
|
|
8151
|
-
expire(
|
|
8152
|
-
if (
|
|
8153
|
-
|
|
8230
|
+
expire(plan2) {
|
|
8231
|
+
if (plan2.state !== "ready" && plan2.state !== "committing") return;
|
|
8232
|
+
plan2.state = "expired";
|
|
8154
8233
|
}
|
|
8155
8234
|
sweep() {
|
|
8156
8235
|
const now = Date.now();
|
|
8157
|
-
for (const [id,
|
|
8158
|
-
if (
|
|
8159
|
-
|
|
8160
|
-
|
|
8236
|
+
for (const [id, plan2] of this.plans) {
|
|
8237
|
+
if (now >= plan2.expiresAt && plan2.state !== "committing") this.plans.delete(id);
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
};
|
|
8241
|
+
|
|
8242
|
+
// src/domain/result.ts
|
|
8243
|
+
function isApiError(value) {
|
|
8244
|
+
if (Array.isArray(value)) return value.some(isApiError);
|
|
8245
|
+
if (!value || typeof value !== "object") return false;
|
|
8246
|
+
const record3 = value;
|
|
8247
|
+
return "error" in record3 && record3.error !== null && typeof record3.error === "object";
|
|
8248
|
+
}
|
|
8249
|
+
function parseJson(text4) {
|
|
8250
|
+
const lines = text4.split("\n");
|
|
8251
|
+
const start2 = lines.findIndex((line) => /^[[{]/.test(line.trim()));
|
|
8252
|
+
if (start2 < 0) return void 0;
|
|
8253
|
+
const candidate = lines.slice(start2).join("\n").trim();
|
|
8254
|
+
try {
|
|
8255
|
+
return JSON.parse(candidate);
|
|
8256
|
+
} catch {
|
|
8257
|
+
const parsed = [];
|
|
8258
|
+
for (const line of lines.slice(start2)) {
|
|
8259
|
+
const value = line.trim();
|
|
8260
|
+
if (!/^[[{]/.test(value)) continue;
|
|
8261
|
+
try {
|
|
8262
|
+
parsed.push(JSON.parse(value));
|
|
8263
|
+
} catch {
|
|
8161
8264
|
}
|
|
8162
8265
|
}
|
|
8266
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
function inlineCap(maxInlineBytes, mode, requested) {
|
|
8270
|
+
const base2 = mode === "full" ? Math.max(maxInlineBytes, 5 * 1024 * 1024) : maxInlineBytes;
|
|
8271
|
+
return requested === void 0 ? base2 : Math.min(base2, requested);
|
|
8272
|
+
}
|
|
8273
|
+
function parseResult(result, options) {
|
|
8274
|
+
const text4 = result.stdout.toString("utf8");
|
|
8275
|
+
const parsed = parseJson(text4);
|
|
8276
|
+
const serialized = parsed === void 0 ? text4 : JSON.stringify(parsed);
|
|
8277
|
+
const bytes2 = Buffer.byteLength(serialized);
|
|
8278
|
+
const cap = inlineCap(options.maxInlineBytes, options.mode, options.requestedInlineBytes);
|
|
8279
|
+
const truncated = bytes2 > cap;
|
|
8280
|
+
const empty = text4.trim().length === 0;
|
|
8281
|
+
return {
|
|
8282
|
+
// A successful DELETE commonly returns 204 with no body. Empty output
|
|
8283
|
+
// with exit 0 is success; non-JSON text is not a JSON result.
|
|
8284
|
+
ok: result.exitCode === 0 && (empty || parsed !== void 0 && !isApiError(parsed)),
|
|
8285
|
+
data: parsed,
|
|
8286
|
+
raw: parsed === void 0 ? text4.slice(0, cap) : void 0,
|
|
8287
|
+
truncated,
|
|
8288
|
+
diagnostics: { exitCode: result.exitCode, durationMs: result.durationMs, bytes: bytes2 }
|
|
8289
|
+
};
|
|
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
|
+
}
|
|
8305
|
+
function itemKey(page2) {
|
|
8306
|
+
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return void 0;
|
|
8307
|
+
const record3 = page2;
|
|
8308
|
+
if (Array.isArray(record3["items"])) return "items";
|
|
8309
|
+
const arrays = Object.keys(record3).filter((key) => Array.isArray(record3[key]));
|
|
8310
|
+
return arrays.length === 1 ? arrays[0] : void 0;
|
|
8311
|
+
}
|
|
8312
|
+
function capPageItems(data, maxItems) {
|
|
8313
|
+
const pages = data === void 0 ? [] : Array.isArray(data) ? data : [data];
|
|
8314
|
+
const keys = pages.map((page2) => itemKey(page2) ?? (isEmptyPage(page2) ? null : void 0));
|
|
8315
|
+
if (keys.some((key) => key === void 0)) return { data: pages, pages: pages.length, capped: false };
|
|
8316
|
+
const kept = [];
|
|
8317
|
+
let items = 0;
|
|
8318
|
+
let capped = false;
|
|
8319
|
+
for (const [index2, page2] of pages.entries()) {
|
|
8320
|
+
if (items >= maxItems) {
|
|
8321
|
+
capped = true;
|
|
8322
|
+
break;
|
|
8323
|
+
}
|
|
8324
|
+
const key = keys[index2];
|
|
8325
|
+
if (!key) {
|
|
8326
|
+
kept.push(page2);
|
|
8327
|
+
continue;
|
|
8328
|
+
}
|
|
8329
|
+
const record3 = page2;
|
|
8330
|
+
const list = record3[key];
|
|
8331
|
+
const room = maxItems - items;
|
|
8332
|
+
if (list.length > room) {
|
|
8333
|
+
const { nextPageToken: _dropped, ...rest } = record3;
|
|
8334
|
+
kept.push({ ...rest, [key]: list.slice(0, room) });
|
|
8335
|
+
items += room;
|
|
8336
|
+
capped = true;
|
|
8337
|
+
break;
|
|
8338
|
+
}
|
|
8339
|
+
kept.push(page2);
|
|
8340
|
+
items += list.length;
|
|
8163
8341
|
}
|
|
8342
|
+
return { data: kept, pages: kept.length, items, capped };
|
|
8343
|
+
}
|
|
8344
|
+
function isEmptyPage(page2) {
|
|
8345
|
+
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return false;
|
|
8346
|
+
return !Object.values(page2).some(Array.isArray);
|
|
8347
|
+
}
|
|
8348
|
+
function redactDryRun(value) {
|
|
8349
|
+
if (Array.isArray(value)) return value.map(redactDryRun);
|
|
8350
|
+
if (value && typeof value === "object") {
|
|
8351
|
+
const out = {};
|
|
8352
|
+
for (const [key, child] of Object.entries(value)) {
|
|
8353
|
+
if (/^(body|raw|content)$/i.test(key)) out[key] = "[redacted]";
|
|
8354
|
+
else out[key] = redactDryRun(child);
|
|
8355
|
+
}
|
|
8356
|
+
return out;
|
|
8357
|
+
}
|
|
8358
|
+
return value;
|
|
8359
|
+
}
|
|
8360
|
+
function apiErrorOf(result, parsed) {
|
|
8361
|
+
const data = parsed ? parsed.data : parseJson(result.stdout.toString("utf8"));
|
|
8362
|
+
const found = Array.isArray(data) ? data.find(isApiError) : data;
|
|
8363
|
+
if (!isApiError(found)) return void 0;
|
|
8364
|
+
return found.error;
|
|
8365
|
+
}
|
|
8366
|
+
function stderrText(result) {
|
|
8367
|
+
return result.stderr.toString("utf8").split("\n").filter((line) => line.trim() && !/^Using keyring backend:/i.test(line)).join("\n").trim();
|
|
8368
|
+
}
|
|
8369
|
+
var EXIT = { auth: 2, validation: 3, discovery: 4, internal: 5 };
|
|
8370
|
+
var SCOPE_REASONS = /* @__PURE__ */ new Set(["ACCESS_TOKEN_SCOPE_INSUFFICIENT", "insufficientPermissions"]);
|
|
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;
|
|
8372
|
+
function privilegeRefused(result, service, parsed) {
|
|
8373
|
+
if (!isAdminService(service)) return false;
|
|
8374
|
+
if (result.timedOut || result.outputCapped || result.exitCode === 0 || result.exitCode === EXIT.auth) return false;
|
|
8375
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8376
|
+
if (error2?.code !== void 0 && error2.code !== 401 && error2.code !== 403) return false;
|
|
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-");
|
|
8382
|
+
}
|
|
8383
|
+
var ADMIN_PRIVILEGES = { "admin-reports": "Reports" };
|
|
8384
|
+
function privilegeName(service) {
|
|
8385
|
+
const name = service ? ADMIN_PRIVILEGES[service] : void 0;
|
|
8386
|
+
return name ? `the ${name} privilege` : "the admin privilege it needs";
|
|
8387
|
+
}
|
|
8388
|
+
function authProblem(result, service, parsed) {
|
|
8389
|
+
if (result.timedOut || result.outputCapped) return void 0;
|
|
8390
|
+
if (privilegeRefused(result, service, parsed)) return void 0;
|
|
8391
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8392
|
+
if (result.exitCode === 0 && !error2) return void 0;
|
|
8393
|
+
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
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)) {
|
|
8396
|
+
return "AUTH_SCOPE_MISSING";
|
|
8397
|
+
}
|
|
8398
|
+
if (error2?.code === 401 || error2?.status === "UNAUTHENTICATED" || reasons.includes("authError") || /invalid_grant|\bUNAUTHENTICATED\b|^error\[auth\]/im.test(text4)) {
|
|
8399
|
+
return "AUTH_REQUIRED";
|
|
8400
|
+
}
|
|
8401
|
+
return void 0;
|
|
8402
|
+
}
|
|
8403
|
+
var GENERIC_REMEDIATION = {
|
|
8404
|
+
signIn: "sign in to the account again",
|
|
8405
|
+
consent: "sign in again and grant the scope this operation needs",
|
|
8406
|
+
privilege: "sign in with a Workspace admin account that has the admin privilege this operation needs, or remove the service from enabledServices"
|
|
8164
8407
|
};
|
|
8408
|
+
var DISABLED_REASONS = /* @__PURE__ */ new Set(["accessNotConfigured", "SERVICE_DISABLED"]);
|
|
8409
|
+
var CONSOLE_URL = /https:\/\/console\.(?:developers|cloud)\.google\.com\/[^\s"'<>)]+/;
|
|
8410
|
+
function disabledApi(result, parsed) {
|
|
8411
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8412
|
+
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
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)) {
|
|
8415
|
+
return void 0;
|
|
8416
|
+
}
|
|
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];
|
|
8419
|
+
return { ...enableUrl ? { enableUrl } : {}, ...project ? { project } : {} };
|
|
8420
|
+
}
|
|
8421
|
+
function processError(result, parsed, service, remediation = GENERIC_REMEDIATION) {
|
|
8422
|
+
if (result.timedOut) return new GwsError("TIMEOUT", `gws did not finish within the configured execution.timeoutMs (${result.durationMs}ms elapsed)`);
|
|
8423
|
+
if (result.outputCapped) return new GwsError("OUTPUT_LIMIT", "gws output exceeded execution.maxStdoutBytes/maxStderrBytes; narrow fields or pagination");
|
|
8424
|
+
const stderr = stderrText(result);
|
|
8425
|
+
const payload = apiErrorOf(result, parsed);
|
|
8426
|
+
const message = payload?.message || stderr || "gws command failed";
|
|
8427
|
+
if (privilegeRefused(result, service, parsed)) {
|
|
8428
|
+
return new GwsError(
|
|
8429
|
+
"ADMIN_PRIVILEGE_REQUIRED",
|
|
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)})`,
|
|
8431
|
+
{ next: `${remediation.privilege.charAt(0).toUpperCase()}${remediation.privilege.slice(1)}.` }
|
|
8432
|
+
);
|
|
8433
|
+
}
|
|
8434
|
+
const problem = authProblem(result, service, parsed) ?? (result.exitCode === EXIT.auth ? "AUTH_REQUIRED" : void 0);
|
|
8435
|
+
if (problem === "AUTH_SCOPE_MISSING") {
|
|
8436
|
+
return new GwsError("AUTH_SCOPE_MISSING", `the credential lacks a scope for this operation; ${remediation.consent} (gws: ${message.slice(0, 300)})`);
|
|
8437
|
+
}
|
|
8438
|
+
if (problem === "AUTH_REQUIRED") {
|
|
8439
|
+
return new GwsError("AUTH_REQUIRED", `the Google Workspace sign-in is missing or no longer valid; ${remediation.signIn} (gws: ${message.slice(0, 300)})`);
|
|
8440
|
+
}
|
|
8441
|
+
const disabled = disabledApi(result, parsed);
|
|
8442
|
+
if (disabled) {
|
|
8443
|
+
const where = disabled.project ? ` for project ${disabled.project}` : "";
|
|
8444
|
+
const next = disabled.enableUrl ? `Ask the user to open ${disabled.enableUrl} and enable the API${where}, then wait about a minute and retry.` : `Ask the user to enable the API${where} in the Google Cloud console, then wait about a minute and retry.`;
|
|
8445
|
+
return new GwsError("API_DISABLED", message.slice(0, 500), { next, ...disabled.enableUrl ? { enableUrl: disabled.enableUrl } : {} });
|
|
8446
|
+
}
|
|
8447
|
+
if (result.exitCode === EXIT.validation) return new GwsError("GWS_VALIDATION_ERROR", `gws rejected the request: ${message.slice(0, 500)}`);
|
|
8448
|
+
if (result.exitCode === EXIT.discovery) return new GwsError("GWS_DISCOVERY_ERROR", `gws could not load the API description: ${message.slice(0, 500)}`);
|
|
8449
|
+
if (result.exitCode === EXIT.internal) return new GwsError("GWS_INTERNAL_ERROR", `gws failed: ${message.slice(0, 500)}`);
|
|
8450
|
+
return new GwsError("GWS_API_ERROR", message.slice(0, 500));
|
|
8451
|
+
}
|
|
8165
8452
|
|
|
8166
8453
|
// src/host/commands.ts
|
|
8167
8454
|
var COMMANDS = {
|
|
8168
8455
|
status: { name: "gws-status", description: "Google Workspace: sign-in, access, pending plans, setup steps, and unguarded agents" },
|
|
8169
8456
|
doctor: { name: "gws-doctor", description: "Google Workspace: check every account's services with one small read each" }
|
|
8170
8457
|
};
|
|
8171
|
-
function planSummary(
|
|
8458
|
+
function planSummary(plan2) {
|
|
8172
8459
|
return {
|
|
8173
|
-
planId:
|
|
8174
|
-
sessionID:
|
|
8175
|
-
category:
|
|
8176
|
-
operation:
|
|
8177
|
-
account:
|
|
8178
|
-
preview: redactedPreview(
|
|
8179
|
-
createdAt: new Date(
|
|
8180
|
-
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()
|
|
8181
8468
|
};
|
|
8182
8469
|
}
|
|
8183
8470
|
function summarizeStatus(status2, plans = []) {
|
|
@@ -8219,6 +8506,7 @@ function accountLines(account2) {
|
|
|
8219
8506
|
lines.push(`Services: ${account2.services.join(", ") || "none"}`);
|
|
8220
8507
|
if (account2.grantedScopes) lines.push(`Granted scopes: ${scopeList(account2.grantedScopes)}`);
|
|
8221
8508
|
if (account2.missingScopes?.length) lines.push(`Scopes not granted: ${scopeList(account2.missingScopes)}`);
|
|
8509
|
+
if (account2.delegationRequired?.length) lines.push(`${DELEGATION_LABEL}: ${delegationText(account2)}`);
|
|
8222
8510
|
return lines;
|
|
8223
8511
|
}
|
|
8224
8512
|
function formatStatus(status2, plans = [], now = Date.now()) {
|
|
@@ -8235,7 +8523,7 @@ function formatStatus(status2, plans = [], now = Date.now()) {
|
|
|
8235
8523
|
lines.push(`Agents without default rules: ${status2.unguardedAgents.join(", ")}`, status2.permissionsBlock ?? "", BLOCK_PLACEMENT);
|
|
8236
8524
|
}
|
|
8237
8525
|
if (status2.auditDegraded) lines.push("Audit: some records could not be saved, so the audit log may be incomplete.");
|
|
8238
|
-
lines.push(`OpenCode: ${openCodeLabel(status2)}`, `Next step: ${status2.nextStep
|
|
8526
|
+
lines.push(`OpenCode: ${openCodeLabel(status2)}`, `Next step: ${status2.nextStep}`);
|
|
8239
8527
|
return lines.join("\n");
|
|
8240
8528
|
}
|
|
8241
8529
|
function planLines(plans, now) {
|
|
@@ -8244,14 +8532,15 @@ function planLines(plans, now) {
|
|
|
8244
8532
|
lines.push("No pending plans in this session.");
|
|
8245
8533
|
} else {
|
|
8246
8534
|
lines.push(`Pending plans in this session (${plans.length}):`);
|
|
8247
|
-
for (const
|
|
8535
|
+
for (const plan2 of plans) {
|
|
8248
8536
|
const details = [
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
`
|
|
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)}`
|
|
8253
8542
|
].filter(Boolean);
|
|
8254
|
-
lines.push(`- ${
|
|
8543
|
+
lines.push(`- ${plan2.category} ${plan2.operation} as ${plan2.account}: ${details.join(" \xB7 ")} (${plan2.planId})`);
|
|
8255
8544
|
}
|
|
8256
8545
|
lines.push("Approval happens only in the OpenCode permission prompt when the agent commits a plan. Pending plans expire automatically.");
|
|
8257
8546
|
}
|
|
@@ -8289,6 +8578,8 @@ function nextFor(outcome) {
|
|
|
8289
8578
|
return `enable it at ${outcome.enableUrl}, wait about a minute, and run /gws-doctor again`;
|
|
8290
8579
|
case "DENIED":
|
|
8291
8580
|
return "allow gws.read for this account in your permissions, or skip the check";
|
|
8581
|
+
case "ADMIN_PRIVILEGE_REQUIRED":
|
|
8582
|
+
return `sign in with a Workspace admin account that has ${privilegeName(outcome.service)}, or remove ${outcome.service} from accounts.${outcome.account}.enabledServices`;
|
|
8292
8583
|
default:
|
|
8293
8584
|
return void 0;
|
|
8294
8585
|
}
|
|
@@ -8298,8 +8589,8 @@ function formatDoctor(outcomes) {
|
|
|
8298
8589
|
if (outcomes.length === 0) lines.push("No account enables a service.");
|
|
8299
8590
|
for (const outcome of outcomes) {
|
|
8300
8591
|
const next = nextFor(outcome);
|
|
8301
|
-
const
|
|
8302
|
-
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}` : ""}`);
|
|
8303
8594
|
}
|
|
8304
8595
|
return lines.join("\n");
|
|
8305
8596
|
}
|
|
@@ -8310,9 +8601,9 @@ function summarizeDoctor(outcomes) {
|
|
|
8310
8601
|
}
|
|
8311
8602
|
async function registerCommands(ctx, sources, signal) {
|
|
8312
8603
|
const lifetime = new Lifecycle();
|
|
8313
|
-
const post = (sessionID, delivery,
|
|
8604
|
+
const post = (sessionID, delivery, text4, description, command) => lifetime.wait(() => ctx.session.synthetic({
|
|
8314
8605
|
sessionID,
|
|
8315
|
-
text:
|
|
8606
|
+
text: text4,
|
|
8316
8607
|
description,
|
|
8317
8608
|
metadata: { source: "opencode-gws", command },
|
|
8318
8609
|
delivery,
|
|
@@ -8424,6 +8715,11 @@ function accessOptions(profile) {
|
|
|
8424
8715
|
if (!offersWrite(profile)) return [read];
|
|
8425
8716
|
return [read, { value: "write", label: "Read and write", description: `Requests: ${signInScopes(profile, "write").join(" ")}` }];
|
|
8426
8717
|
}
|
|
8718
|
+
function delegationNote(alias, profile) {
|
|
8719
|
+
const services = delegationRequired(profile.enabledServices, profile.scopes);
|
|
8720
|
+
if (services.length === 0) return "";
|
|
8721
|
+
return ` ${services.join(", ")} needs domain-wide delegation, which a Workspace admin sets up, so this sign-in requests none of its scopes; with delegation, add them to accounts.${alias}.scopes.`;
|
|
8722
|
+
}
|
|
8427
8723
|
function defaultAccess(profile) {
|
|
8428
8724
|
const access = profile.access;
|
|
8429
8725
|
return access === "write" || typeof access === "object" && Object.values(access).includes("write") ? "write" : "read";
|
|
@@ -8500,7 +8796,7 @@ function signInForm(alias, profile, configured, redirect = {}) {
|
|
|
8500
8796
|
key: "access",
|
|
8501
8797
|
type: "string",
|
|
8502
8798
|
title: "Access",
|
|
8503
|
-
description: `What ${alias} may do in Google Workspace. Read-only cannot change, send, share, or delete anything
|
|
8799
|
+
description: `What ${alias} may do in Google Workspace. Read-only cannot change, send, share, or delete anything.${delegationNote(alias, profile)}`,
|
|
8504
8800
|
required: true,
|
|
8505
8801
|
default: defaultAccess(profile),
|
|
8506
8802
|
options: accessOptions(profile)
|
|
@@ -8659,8 +8955,8 @@ function tokenError(status2, json, secrets) {
|
|
|
8659
8955
|
message = scrubSecrets(message, secrets);
|
|
8660
8956
|
return new Error(message);
|
|
8661
8957
|
}
|
|
8662
|
-
function scrubSecrets(
|
|
8663
|
-
let out =
|
|
8958
|
+
function scrubSecrets(text4, secrets) {
|
|
8959
|
+
let out = text4;
|
|
8664
8960
|
for (const secret of secrets) {
|
|
8665
8961
|
if (secret) out = out.split(secret).join("[redacted]");
|
|
8666
8962
|
}
|
|
@@ -8682,15 +8978,15 @@ async function postToken(endpoint, body, fetchImpl, secrets) {
|
|
|
8682
8978
|
} catch {
|
|
8683
8979
|
throw new Error("Google token request failed: could not reach the token endpoint.");
|
|
8684
8980
|
}
|
|
8685
|
-
let
|
|
8981
|
+
let text4 = "";
|
|
8686
8982
|
try {
|
|
8687
|
-
|
|
8983
|
+
text4 = await response.text();
|
|
8688
8984
|
} catch {
|
|
8689
8985
|
}
|
|
8690
8986
|
let json = {};
|
|
8691
|
-
if (
|
|
8987
|
+
if (text4.trim()) {
|
|
8692
8988
|
try {
|
|
8693
|
-
const parsed = JSON.parse(
|
|
8989
|
+
const parsed = JSON.parse(text4);
|
|
8694
8990
|
if (isRecord(parsed)) json = parsed;
|
|
8695
8991
|
} catch {
|
|
8696
8992
|
if (response.ok) throw new Error("Google token endpoint returned an invalid response.");
|
|
@@ -9150,22 +9446,27 @@ function nextStep(accounts, agents) {
|
|
|
9150
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.";
|
|
9151
9447
|
return void 0;
|
|
9152
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
|
+
}
|
|
9153
9454
|
function statusSource(runtime, listAgents) {
|
|
9154
9455
|
return async (signal) => {
|
|
9155
9456
|
const base2 = await runtime.status(signal);
|
|
9156
9457
|
const agents = unguardedAgents(await runtime.wait(() => listAgents(), signal));
|
|
9157
9458
|
const next = nextStep(base2.accounts, agents);
|
|
9158
9459
|
return {
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9460
|
+
version: base2.version,
|
|
9461
|
+
host: base2.host,
|
|
9462
|
+
state: stateOf(base2.accounts, base2.host.tested),
|
|
9162
9463
|
gws: { path: base2.binary.path, version: base2.binary.version },
|
|
9163
9464
|
services: base2.services,
|
|
9164
9465
|
accounts: base2.accounts,
|
|
9165
9466
|
auditDegraded: base2.auditDegraded,
|
|
9166
9467
|
unguardedAgents: agents,
|
|
9167
9468
|
...agents.length > 0 ? { permissionsBlock: permissionsBlock(agents) } : {},
|
|
9168
|
-
|
|
9469
|
+
nextStep: next ?? NOTHING_TO_DO
|
|
9169
9470
|
};
|
|
9170
9471
|
};
|
|
9171
9472
|
}
|
|
@@ -9210,25 +9511,25 @@ function operationRequest(call2, action, target) {
|
|
|
9210
9511
|
metadata: compact({ ...mark(call2), account: target.account, operation: target.operation, service: target.service, category: target.category })
|
|
9211
9512
|
};
|
|
9212
9513
|
}
|
|
9213
|
-
function planMetadata(
|
|
9514
|
+
function planMetadata(plan2) {
|
|
9214
9515
|
return compact({
|
|
9215
|
-
planId:
|
|
9216
|
-
category:
|
|
9217
|
-
operation:
|
|
9218
|
-
account:
|
|
9219
|
-
...redactedPreview(
|
|
9220
|
-
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()
|
|
9221
9522
|
});
|
|
9222
9523
|
}
|
|
9223
9524
|
var SAVABLE = /* @__PURE__ */ new Set(["write", "export"]);
|
|
9224
|
-
function commitRequests(call2, action,
|
|
9225
|
-
const id = resource(
|
|
9226
|
-
const request = { ...base(call2, action), resources: [id], metadata: planMetadata(
|
|
9227
|
-
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];
|
|
9228
9529
|
return [request];
|
|
9229
9530
|
}
|
|
9230
|
-
function compact(
|
|
9231
|
-
return Object.fromEntries(Object.entries(
|
|
9531
|
+
function compact(record3) {
|
|
9532
|
+
return Object.fromEntries(Object.entries(record3).filter(([, value]) => value !== void 0));
|
|
9232
9533
|
}
|
|
9233
9534
|
|
|
9234
9535
|
// src/host/tools.ts
|
|
@@ -9304,6 +9605,9 @@ var Diagnostics = object({
|
|
|
9304
9605
|
bytes: number2().describe("Serialized size of data")
|
|
9305
9606
|
}).describe("gws process facts");
|
|
9306
9607
|
var FileInfo = object({ path: string2(), bytes: number2(), sha256: string2() }).describe("A local file the plugin delivered");
|
|
9608
|
+
var Restriction = _enum(["app-only", "domain-wide-delegation"]).describe(
|
|
9609
|
+
"Why an ordinary Google sign-in cannot run this method: app-only needs a Chat app's or Classroom add-on's own credentials; domain-wide-delegation needs scopes a Workspace admin grants only through domain-wide delegation"
|
|
9610
|
+
);
|
|
9307
9611
|
var CatalogEntry = object({
|
|
9308
9612
|
kind: _enum(["service", "method", "helper"]),
|
|
9309
9613
|
id: string2().describe("Pass a method or helper id as operation"),
|
|
@@ -9315,7 +9619,8 @@ var CatalogEntry = object({
|
|
|
9315
9619
|
executable: boolean2().describe("The plugin can run it (a service: at least one of its operations); false for denied, deploy, and watch entries"),
|
|
9316
9620
|
commitTool: string2().optional().describe('The tool that commits its plan, e.g. "gws_send"; absent for reads'),
|
|
9317
9621
|
alternative: string2().optional().describe("For a helper that cannot run: the method id that does the same job"),
|
|
9318
|
-
appOnly: literal(true).optional().describe("Only an app's own credentials (a Chat app or a Classroom add-on) can run it; a Google sign-in cannot")
|
|
9622
|
+
appOnly: literal(true).optional().describe("Only an app's own credentials (a Chat app or a Classroom add-on) can run it; a Google sign-in cannot"),
|
|
9623
|
+
restriction: Restriction.optional()
|
|
9319
9624
|
});
|
|
9320
9625
|
var CatalogOutput = object({
|
|
9321
9626
|
entries: array(CatalogEntry),
|
|
@@ -9343,6 +9648,7 @@ var SchemaOutput = object({
|
|
|
9343
9648
|
account: string2(),
|
|
9344
9649
|
schema: ResolvedSchema.optional().describe("Present for a method"),
|
|
9345
9650
|
appOnly: literal(true).optional().describe("Only an app's own credentials (a Chat app or a Classroom add-on) can run this method; a Google sign-in cannot"),
|
|
9651
|
+
restriction: Restriction.optional(),
|
|
9346
9652
|
helper: HelperEntry.extend({
|
|
9347
9653
|
input: unknown().optional().describe("JSON Schema of the helper's input flags; absent when the helper cannot run")
|
|
9348
9654
|
}).optional().describe("Present for a helper")
|
|
@@ -9366,7 +9672,8 @@ var Preview = object({
|
|
|
9366
9672
|
url: string2().nullable().optional(),
|
|
9367
9673
|
params: JsonObject.describe("Parameters with secret-like values redacted"),
|
|
9368
9674
|
bodySummary: string2().optional().describe("Body keys and size; never the body"),
|
|
9369
|
-
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"),
|
|
9370
9677
|
uploads: array(string2()).optional().describe("Local files the commit uploads or attaches"),
|
|
9371
9678
|
destination: string2().optional().describe("Local file the export writes"),
|
|
9372
9679
|
overwrite: boolean2().optional().describe("The export replaces an existing file at destination"),
|
|
@@ -9392,177 +9699,6 @@ var CommitOutput = object({
|
|
|
9392
9699
|
cleanupDegraded: boolean2().optional().describe("The change succeeded, but private staging cleanup failed; do not retry the mutation")
|
|
9393
9700
|
});
|
|
9394
9701
|
|
|
9395
|
-
// src/domain/result.ts
|
|
9396
|
-
function isApiError(value) {
|
|
9397
|
-
if (Array.isArray(value)) return value.some(isApiError);
|
|
9398
|
-
if (!value || typeof value !== "object") return false;
|
|
9399
|
-
const record2 = value;
|
|
9400
|
-
return "error" in record2 && record2.error !== null && typeof record2.error === "object";
|
|
9401
|
-
}
|
|
9402
|
-
function parseJson(text3) {
|
|
9403
|
-
const lines = text3.split("\n");
|
|
9404
|
-
const start2 = lines.findIndex((line) => /^[[{]/.test(line.trim()));
|
|
9405
|
-
if (start2 < 0) return void 0;
|
|
9406
|
-
const candidate = lines.slice(start2).join("\n").trim();
|
|
9407
|
-
try {
|
|
9408
|
-
return JSON.parse(candidate);
|
|
9409
|
-
} catch {
|
|
9410
|
-
const parsed = [];
|
|
9411
|
-
for (const line of lines.slice(start2)) {
|
|
9412
|
-
const value = line.trim();
|
|
9413
|
-
if (!/^[[{]/.test(value)) continue;
|
|
9414
|
-
try {
|
|
9415
|
-
parsed.push(JSON.parse(value));
|
|
9416
|
-
} catch {
|
|
9417
|
-
}
|
|
9418
|
-
}
|
|
9419
|
-
return parsed.length > 0 ? parsed : void 0;
|
|
9420
|
-
}
|
|
9421
|
-
}
|
|
9422
|
-
function inlineCap(maxInlineBytes, mode, requested) {
|
|
9423
|
-
const base2 = mode === "full" ? Math.max(maxInlineBytes, 5 * 1024 * 1024) : maxInlineBytes;
|
|
9424
|
-
return requested === void 0 ? base2 : Math.min(base2, requested);
|
|
9425
|
-
}
|
|
9426
|
-
function parseResult(result, options) {
|
|
9427
|
-
const text3 = result.stdout.toString("utf8");
|
|
9428
|
-
const parsed = parseJson(text3);
|
|
9429
|
-
const serialized = parsed === void 0 ? text3 : JSON.stringify(parsed);
|
|
9430
|
-
const bytes2 = Buffer.byteLength(serialized);
|
|
9431
|
-
const cap = inlineCap(options.maxInlineBytes, options.mode, options.requestedInlineBytes);
|
|
9432
|
-
const truncated = bytes2 > cap;
|
|
9433
|
-
const empty = text3.trim().length === 0;
|
|
9434
|
-
return {
|
|
9435
|
-
// A successful DELETE commonly returns 204 with no body. Empty output
|
|
9436
|
-
// with exit 0 is success; non-JSON text is not a JSON result.
|
|
9437
|
-
ok: result.exitCode === 0 && (empty || parsed !== void 0 && !isApiError(parsed)),
|
|
9438
|
-
data: parsed,
|
|
9439
|
-
raw: parsed === void 0 ? text3.slice(0, cap) : void 0,
|
|
9440
|
-
truncated,
|
|
9441
|
-
diagnostics: { exitCode: result.exitCode, durationMs: result.durationMs, bytes: bytes2 }
|
|
9442
|
-
};
|
|
9443
|
-
}
|
|
9444
|
-
function itemKey(page2) {
|
|
9445
|
-
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return void 0;
|
|
9446
|
-
const record2 = page2;
|
|
9447
|
-
if (Array.isArray(record2["items"])) return "items";
|
|
9448
|
-
const arrays = Object.keys(record2).filter((key) => Array.isArray(record2[key]));
|
|
9449
|
-
return arrays.length === 1 ? arrays[0] : void 0;
|
|
9450
|
-
}
|
|
9451
|
-
function capPageItems(data, maxItems) {
|
|
9452
|
-
const pages = data === void 0 ? [] : Array.isArray(data) ? data : [data];
|
|
9453
|
-
const keys = pages.map((page2) => itemKey(page2) ?? (isEmptyPage(page2) ? null : void 0));
|
|
9454
|
-
if (keys.some((key) => key === void 0)) return { data: pages, pages: pages.length, capped: false };
|
|
9455
|
-
const kept = [];
|
|
9456
|
-
let items = 0;
|
|
9457
|
-
let capped = false;
|
|
9458
|
-
for (const [index2, page2] of pages.entries()) {
|
|
9459
|
-
if (items >= maxItems) {
|
|
9460
|
-
capped = true;
|
|
9461
|
-
break;
|
|
9462
|
-
}
|
|
9463
|
-
const key = keys[index2];
|
|
9464
|
-
if (!key) {
|
|
9465
|
-
kept.push(page2);
|
|
9466
|
-
continue;
|
|
9467
|
-
}
|
|
9468
|
-
const record2 = page2;
|
|
9469
|
-
const list = record2[key];
|
|
9470
|
-
const room = maxItems - items;
|
|
9471
|
-
if (list.length > room) {
|
|
9472
|
-
const { nextPageToken: _dropped, ...rest } = record2;
|
|
9473
|
-
kept.push({ ...rest, [key]: list.slice(0, room) });
|
|
9474
|
-
items += room;
|
|
9475
|
-
capped = true;
|
|
9476
|
-
break;
|
|
9477
|
-
}
|
|
9478
|
-
kept.push(page2);
|
|
9479
|
-
items += list.length;
|
|
9480
|
-
}
|
|
9481
|
-
return { data: kept, pages: kept.length, items, capped };
|
|
9482
|
-
}
|
|
9483
|
-
function isEmptyPage(page2) {
|
|
9484
|
-
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return false;
|
|
9485
|
-
return !Object.values(page2).some(Array.isArray);
|
|
9486
|
-
}
|
|
9487
|
-
function redactDryRun(value) {
|
|
9488
|
-
if (Array.isArray(value)) return value.map(redactDryRun);
|
|
9489
|
-
if (value && typeof value === "object") {
|
|
9490
|
-
const out = {};
|
|
9491
|
-
for (const [key, child] of Object.entries(value)) {
|
|
9492
|
-
if (/^(body|raw|content)$/i.test(key)) out[key] = "[redacted]";
|
|
9493
|
-
else out[key] = redactDryRun(child);
|
|
9494
|
-
}
|
|
9495
|
-
return out;
|
|
9496
|
-
}
|
|
9497
|
-
return value;
|
|
9498
|
-
}
|
|
9499
|
-
function apiErrorOf(result, parsed) {
|
|
9500
|
-
const data = parsed ? parsed.data : parseJson(result.stdout.toString("utf8"));
|
|
9501
|
-
const found = Array.isArray(data) ? data.find(isApiError) : data;
|
|
9502
|
-
if (!isApiError(found)) return void 0;
|
|
9503
|
-
return found.error;
|
|
9504
|
-
}
|
|
9505
|
-
function stderrText(result) {
|
|
9506
|
-
return result.stderr.toString("utf8").split("\n").filter((line) => line.trim() && !/^Using keyring backend:/i.test(line)).join("\n").trim();
|
|
9507
|
-
}
|
|
9508
|
-
var SCOPE_REASONS = /* @__PURE__ */ new Set(["ACCESS_TOKEN_SCOPE_INSUFFICIENT", "insufficientPermissions"]);
|
|
9509
|
-
function authProblem(result, parsed) {
|
|
9510
|
-
if (result.timedOut || result.outputCapped) return void 0;
|
|
9511
|
-
const error2 = apiErrorOf(result, parsed);
|
|
9512
|
-
if (result.exitCode === 0 && !error2) return void 0;
|
|
9513
|
-
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
9514
|
-
const text3 = [error2?.message, error2?.status, stderrText(result)].filter(Boolean).join("\n");
|
|
9515
|
-
if (reasons.some((reason) => reason !== void 0 && SCOPE_REASONS.has(reason)) || /insufficient authentication scopes|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(text3)) {
|
|
9516
|
-
return "AUTH_SCOPE_MISSING";
|
|
9517
|
-
}
|
|
9518
|
-
if (error2?.code === 401 || error2?.status === "UNAUTHENTICATED" || reasons.includes("authError") || /invalid_grant|\bUNAUTHENTICATED\b|^error\[auth\]/im.test(text3)) {
|
|
9519
|
-
return "AUTH_REQUIRED";
|
|
9520
|
-
}
|
|
9521
|
-
return void 0;
|
|
9522
|
-
}
|
|
9523
|
-
var GENERIC_REMEDIATION = {
|
|
9524
|
-
signIn: "sign in to the account again",
|
|
9525
|
-
consent: "sign in again and grant the scope this operation needs"
|
|
9526
|
-
};
|
|
9527
|
-
var EXIT = { auth: 2, validation: 3, discovery: 4, internal: 5 };
|
|
9528
|
-
var DISABLED_REASONS = /* @__PURE__ */ new Set(["accessNotConfigured", "SERVICE_DISABLED"]);
|
|
9529
|
-
var CONSOLE_URL = /https:\/\/console\.(?:developers|cloud)\.google\.com\/[^\s"'<>)]+/;
|
|
9530
|
-
function disabledApi(result, parsed) {
|
|
9531
|
-
const error2 = apiErrorOf(result, parsed);
|
|
9532
|
-
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
9533
|
-
const text3 = [error2?.message, stderrText(result)].filter(Boolean).join("\n");
|
|
9534
|
-
if (!reasons.some((reason) => reason !== void 0 && DISABLED_REASONS.has(reason)) && !/accessnotconfigured|has not been used|is disabled/i.test(text3)) {
|
|
9535
|
-
return void 0;
|
|
9536
|
-
}
|
|
9537
|
-
const enableUrl = (typeof error2?.enable_url === "string" && CONSOLE_URL.test(error2.enable_url) ? error2.enable_url : CONSOLE_URL.exec(text3)?.[0])?.replace(/[.,;]+$/, "");
|
|
9538
|
-
const project = (enableUrl ? /[?&]project=([\w.:-]+)/.exec(enableUrl)?.[1] : void 0) ?? /\bin project ([\w.:-]+)/.exec(text3)?.[1];
|
|
9539
|
-
return { ...enableUrl ? { enableUrl } : {}, ...project ? { project } : {} };
|
|
9540
|
-
}
|
|
9541
|
-
function processError(result, parsed, remediation = GENERIC_REMEDIATION) {
|
|
9542
|
-
if (result.timedOut) return new GwsError("TIMEOUT", `gws did not finish within the configured execution.timeoutMs (${result.durationMs}ms elapsed)`);
|
|
9543
|
-
if (result.outputCapped) return new GwsError("OUTPUT_LIMIT", "gws output exceeded execution.maxStdoutBytes/maxStderrBytes; narrow fields or pagination");
|
|
9544
|
-
const stderr = stderrText(result);
|
|
9545
|
-
const payload = apiErrorOf(result, parsed);
|
|
9546
|
-
const message = payload?.message || stderr || "gws command failed";
|
|
9547
|
-
const problem = authProblem(result, parsed) ?? (result.exitCode === EXIT.auth ? "AUTH_REQUIRED" : void 0);
|
|
9548
|
-
if (problem === "AUTH_SCOPE_MISSING") {
|
|
9549
|
-
return new GwsError("AUTH_SCOPE_MISSING", `the credential lacks a scope for this operation; ${remediation.consent} (gws: ${message.slice(0, 300)})`);
|
|
9550
|
-
}
|
|
9551
|
-
if (problem === "AUTH_REQUIRED") {
|
|
9552
|
-
return new GwsError("AUTH_REQUIRED", `the Google Workspace sign-in is missing or no longer valid; ${remediation.signIn} (gws: ${message.slice(0, 300)})`);
|
|
9553
|
-
}
|
|
9554
|
-
const disabled = disabledApi(result, parsed);
|
|
9555
|
-
if (disabled) {
|
|
9556
|
-
const where = disabled.project ? ` for project ${disabled.project}` : "";
|
|
9557
|
-
const next = disabled.enableUrl ? `Ask the user to open ${disabled.enableUrl} and enable the API${where}, then wait about a minute and retry.` : `Ask the user to enable the API${where} in the Google Cloud console, then wait about a minute and retry.`;
|
|
9558
|
-
return new GwsError("API_DISABLED", message.slice(0, 500), { next, ...disabled.enableUrl ? { enableUrl: disabled.enableUrl } : {} });
|
|
9559
|
-
}
|
|
9560
|
-
if (result.exitCode === EXIT.validation) return new GwsError("GWS_VALIDATION_ERROR", `gws rejected the request: ${message.slice(0, 500)}`);
|
|
9561
|
-
if (result.exitCode === EXIT.discovery) return new GwsError("GWS_DISCOVERY_ERROR", `gws could not load the API description: ${message.slice(0, 500)}`);
|
|
9562
|
-
if (result.exitCode === EXIT.internal) return new GwsError("GWS_INTERNAL_ERROR", `gws failed: ${message.slice(0, 500)}`);
|
|
9563
|
-
return new GwsError("GWS_API_ERROR", message.slice(0, 500));
|
|
9564
|
-
}
|
|
9565
|
-
|
|
9566
9702
|
// src/domain/status.ts
|
|
9567
9703
|
var AccountStatus = object({
|
|
9568
9704
|
alias: string2(),
|
|
@@ -9575,19 +9711,26 @@ var AccountStatus = object({
|
|
|
9575
9711
|
clientSource: _enum(["account", "plugin", "pasted"]).optional().describe("Where the sign-in's OAuth client comes from"),
|
|
9576
9712
|
grantedScopes: array(string2()).optional().describe("Scopes Google granted to the current sign-in"),
|
|
9577
9713
|
missingScopes: array(string2()).optional().describe("Scopes the enabled services need at this access level that were not granted"),
|
|
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"),
|
|
9578
9715
|
nextStep: string2().optional().describe("What to do next; absent when nothing is needed")
|
|
9579
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"];
|
|
9580
9723
|
var StatusOutput = object({
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
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"),
|
|
9584
9727
|
gws: object({ path: string2(), version: string2() }),
|
|
9585
9728
|
services: array(string2()).describe("Model-facing services"),
|
|
9586
9729
|
accounts: array(AccountStatus),
|
|
9587
9730
|
auditDegraded: boolean2().describe("Audit records were dropped, failed, or exceeded the sink deadline; sticky until reload"),
|
|
9588
9731
|
unguardedAgents: number2().int().describe("How many agents missed the default ask rules"),
|
|
9589
9732
|
permissions: string2().optional().describe("Present when an agent missed the ask rules: where to find the fix"),
|
|
9590
|
-
nextStep: string2().
|
|
9733
|
+
nextStep: string2().describe('The one thing to do next; "Nothing to do." when nothing is needed')
|
|
9591
9734
|
});
|
|
9592
9735
|
|
|
9593
9736
|
// src/host/tools.ts
|
|
@@ -9691,6 +9834,7 @@ function toolSpecs(runtime, assert2, status2) {
|
|
|
9691
9834
|
report(call2, input.operation);
|
|
9692
9835
|
const target = runtime.inspect(input, "plan");
|
|
9693
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" }));
|
|
9694
9838
|
const output = await runtime.plan(input, call2.runtime);
|
|
9695
9839
|
return { output, metadata: { operation: target.operation, category: output.category, account: target.account } };
|
|
9696
9840
|
}
|
|
@@ -9704,11 +9848,11 @@ function toolSpecs(runtime, assert2, status2) {
|
|
|
9704
9848
|
input: CommitInput,
|
|
9705
9849
|
output: CommitOutput,
|
|
9706
9850
|
run: async (input, call2) => {
|
|
9707
|
-
const
|
|
9708
|
-
report(call2,
|
|
9709
|
-
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);
|
|
9710
9854
|
const output = await runtime.commit(category, input, call2.runtime);
|
|
9711
|
-
return { output, metadata: { operation:
|
|
9855
|
+
return { output, metadata: { operation: plan2.operation, category, account: plan2.account } };
|
|
9712
9856
|
}
|
|
9713
9857
|
})
|
|
9714
9858
|
)
|
|
@@ -9785,28 +9929,51 @@ function report(call2, operation) {
|
|
|
9785
9929
|
call2.runtime.progress?.({ status: "resolving", operation });
|
|
9786
9930
|
}
|
|
9787
9931
|
function render(output, maxBytes) {
|
|
9788
|
-
const
|
|
9789
|
-
const bytes2 = Buffer.byteLength(
|
|
9790
|
-
if (bytes2 <= maxBytes) return { text:
|
|
9791
|
-
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");
|
|
9792
9936
|
return {
|
|
9793
9937
|
text: `${cut}
|
|
9794
9938
|
\u2026[truncated: ${maxBytes} of ${bytes2} bytes shown; narrow fields, params or pagination to see the rest]`,
|
|
9795
9939
|
truncated: true
|
|
9796
9940
|
};
|
|
9797
9941
|
}
|
|
9798
|
-
function compact2(
|
|
9799
|
-
return Object.fromEntries(Object.entries(
|
|
9942
|
+
function compact2(record3) {
|
|
9943
|
+
return Object.fromEntries(Object.entries(record3).filter(([, value]) => value !== void 0));
|
|
9800
9944
|
}
|
|
9801
9945
|
|
|
9802
9946
|
// src/host/scripting.ts
|
|
9803
9947
|
var TOOLS_RPC_ID = `${RPC_ID}.tools`;
|
|
9804
9948
|
var COMMIT_TOOLS = /* @__PURE__ */ new Set(["write", "send", "share", "delete", "export", "admin"]);
|
|
9805
9949
|
var UNATTENDED = "A script cannot answer an approval prompt; this needs an allow rule for the session's agent.";
|
|
9806
|
-
var
|
|
9807
|
-
sessionID:
|
|
9808
|
-
|
|
9809
|
-
|
|
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
|
+
);
|
|
9810
9977
|
var Failure = strictObject({ code: string2(), next: string2() });
|
|
9811
9978
|
var Description = strictObject({
|
|
9812
9979
|
id: string2(),
|
|
@@ -9821,6 +9988,12 @@ var Description = strictObject({
|
|
|
9821
9988
|
})
|
|
9822
9989
|
)
|
|
9823
9990
|
});
|
|
9991
|
+
function sessionNotFound(error2) {
|
|
9992
|
+
if (error2 === null || typeof error2 !== "object") return false;
|
|
9993
|
+
const record3 = error2;
|
|
9994
|
+
const tag = record3["_tag"] ?? record3["name"];
|
|
9995
|
+
return tag === "Session.NotFoundError" || tag === "SessionNotFoundError" || record3["status"] === 404 || record3["statusCode"] === 404;
|
|
9996
|
+
}
|
|
9824
9997
|
async function registerScripting(ctx, runtime, status2) {
|
|
9825
9998
|
const assert2 = async (request) => {
|
|
9826
9999
|
try {
|
|
@@ -9836,8 +10009,8 @@ async function registerScripting(ctx, runtime, status2) {
|
|
|
9836
10009
|
try {
|
|
9837
10010
|
agent = (await runtime.wait(() => ctx.session.get({ sessionID }), signal)).agent;
|
|
9838
10011
|
} catch (error2) {
|
|
9839
|
-
if (
|
|
9840
|
-
throw
|
|
10012
|
+
if (sessionNotFound(error2)) throw new GwsError("SESSION_UNKNOWN", `there is no session '${sessionID}'`);
|
|
10013
|
+
throw error2;
|
|
9841
10014
|
}
|
|
9842
10015
|
if (!agent) throw new GwsError("SESSION_AGENT_REQUIRED", `session '${sessionID}' names no agent`);
|
|
9843
10016
|
return { sessionID, agent, script: true };
|
|
@@ -9859,8 +10032,8 @@ async function registerScripting(ctx, runtime, status2) {
|
|
|
9859
10032
|
const contract = Rpc2.define({
|
|
9860
10033
|
id: TOOLS_RPC_ID,
|
|
9861
10034
|
methods: {
|
|
9862
|
-
describe: { input:
|
|
9863
|
-
...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 } }]))
|
|
9864
10037
|
},
|
|
9865
10038
|
events: {}
|
|
9866
10039
|
});
|
|
@@ -9963,12 +10136,12 @@ async function registerSkills(ctx, config2, visibleServices) {
|
|
|
9963
10136
|
}
|
|
9964
10137
|
|
|
9965
10138
|
// src/runtime/index.ts
|
|
9966
|
-
import { mkdtempSync, rmSync
|
|
9967
|
-
import { tmpdir } from "node:os";
|
|
9968
|
-
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";
|
|
9969
10142
|
|
|
9970
10143
|
// src/domain/audit.ts
|
|
9971
|
-
import {
|
|
10144
|
+
import { mkdir, rename, stat, writeFile } from "node:fs/promises";
|
|
9972
10145
|
import { write } from "node:fs";
|
|
9973
10146
|
import { dirname as dirname3 } from "node:path";
|
|
9974
10147
|
var EVENTS = /* @__PURE__ */ new Set(["plugin.loaded", "operation.read", "helper.read", "operation.plan", "operation.commit"]);
|
|
@@ -9977,6 +10150,7 @@ var UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
|
|
9977
10150
|
function auditErrorCode(error2) {
|
|
9978
10151
|
return GwsError.from(error2)?.code ?? UNKNOWN_ERROR;
|
|
9979
10152
|
}
|
|
10153
|
+
var AUDIT_MAX_BYTES = 5 * 1024 * 1024;
|
|
9980
10154
|
var MAX_QUEUE = 64;
|
|
9981
10155
|
var WRITE_TIMEOUT_MS = 100;
|
|
9982
10156
|
var Audit = class {
|
|
@@ -9987,6 +10161,9 @@ var Audit = class {
|
|
|
9987
10161
|
if (this.config.sink === "jsonl" && this.config.path) {
|
|
9988
10162
|
await mkdir(dirname3(this.config.path), { recursive: true, mode: 448 });
|
|
9989
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;
|
|
9990
10167
|
await writeFile(this.config.path, line, { flag: "a", mode: 384, signal });
|
|
9991
10168
|
} else {
|
|
9992
10169
|
await writeStderr(line, signal);
|
|
@@ -10106,9 +10283,9 @@ function assertTrusted(path, projectDirectory) {
|
|
|
10106
10283
|
if (!isAbsolute4(path)) throw new Error(`BINARY_TRUST_FAILED: Binary path must be absolute: ${path}`);
|
|
10107
10284
|
if (!existsSync2(path)) throw new Error(`GWS_UNAVAILABLE: The binary.path option names a missing file: ${path}. Next step: S${OVERRIDE.slice(1)}.`);
|
|
10108
10285
|
const real = realpathSync(path);
|
|
10109
|
-
const
|
|
10110
|
-
if (!
|
|
10111
|
-
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) {
|
|
10112
10289
|
throw new Error("BINARY_TRUST_FAILED: Binary must be executable and not group/world writable");
|
|
10113
10290
|
}
|
|
10114
10291
|
if (projectDirectory && isUnder(resolve(projectDirectory), real)) {
|
|
@@ -10119,13 +10296,13 @@ function assertTrusted(path, projectDirectory) {
|
|
|
10119
10296
|
if (typeof process.getgroups === "function") for (const gid of process.getgroups()) groups.add(gid);
|
|
10120
10297
|
let dir = dirname4(real);
|
|
10121
10298
|
for (; ; ) {
|
|
10122
|
-
const
|
|
10123
|
-
const mode =
|
|
10299
|
+
const stat4 = statSync(dir);
|
|
10300
|
+
const mode = stat4.mode & 511;
|
|
10124
10301
|
if ((mode & 2) !== 0) {
|
|
10125
10302
|
throw new Error(`BINARY_TRUST_FAILED: World-writable ancestor: ${dir} (mode ${mode.toString(8)})`);
|
|
10126
10303
|
}
|
|
10127
|
-
if ((mode & 16) !== 0 && !groups.has(
|
|
10128
|
-
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})`);
|
|
10129
10306
|
}
|
|
10130
10307
|
const parent = dirname4(dir);
|
|
10131
10308
|
if (parent === dir) break;
|
|
@@ -10163,12 +10340,12 @@ function resolvePackagedBinary(platform = currentPlatform(), from = new URL("./p
|
|
|
10163
10340
|
}
|
|
10164
10341
|
const path = join3(dirname4(realpathSync(manifest)), "bin", "gws");
|
|
10165
10342
|
if (!existsSync2(path)) throw new Error(`GWS_UNAVAILABLE: ${name} has no bin/gws. Next step: Reinstall the plugin, or ${OVERRIDE}.`);
|
|
10166
|
-
const
|
|
10167
|
-
const mode =
|
|
10168
|
-
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) {
|
|
10169
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`);
|
|
10170
10347
|
}
|
|
10171
|
-
const identity = `${
|
|
10348
|
+
const identity = `${stat3.size}:${stat3.mtimeMs}:${stat3.ino}`;
|
|
10172
10349
|
if (admitted.get(path) === identity) return path;
|
|
10173
10350
|
if (createHash2("sha256").update(readFileSync3(path)).digest("hex") !== GWS_TARGETS[target].sha256) {
|
|
10174
10351
|
throw new Error(`BINARY_TRUST_FAILED: ${name} bin/gws does not match the pinned ${target} release of gws 0.22.5`);
|
|
@@ -10354,6 +10531,62 @@ function runProcess(options) {
|
|
|
10354
10531
|
});
|
|
10355
10532
|
}
|
|
10356
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
|
+
|
|
10357
10590
|
// src/domain/schema.ts
|
|
10358
10591
|
function parseSchemaOutput(id, stdout, stderr) {
|
|
10359
10592
|
const start2 = stdout.indexOf("{");
|
|
@@ -10408,15 +10641,16 @@ var SchemaClient = class {
|
|
|
10408
10641
|
};
|
|
10409
10642
|
|
|
10410
10643
|
// src/runtime/accounts.ts
|
|
10411
|
-
import {
|
|
10644
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
10645
|
+
import { join as join7 } from "node:path";
|
|
10412
10646
|
|
|
10413
10647
|
// src/domain/digest.ts
|
|
10414
10648
|
function canonicalJson(value) {
|
|
10415
10649
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
10416
10650
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
10417
|
-
const
|
|
10418
|
-
const keys = Object.keys(
|
|
10419
|
-
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(",")}}`;
|
|
10420
10654
|
}
|
|
10421
10655
|
|
|
10422
10656
|
// src/auth/doctor.ts
|
|
@@ -10438,10 +10672,17 @@ var PROBES = {
|
|
|
10438
10672
|
"admin-reports": { operation: "admin-reports.activities.list", params: { userKey: "all", applicationName: "login", maxResults: 1, fields: "items(id)" } },
|
|
10439
10673
|
script: { operation: "script.processes.list", params: { pageSize: 1, fields: "processes(processType)" } }
|
|
10440
10674
|
};
|
|
10441
|
-
function doctorTarget(account2, service) {
|
|
10675
|
+
function doctorTarget(account2, service, needsDelegation = false) {
|
|
10676
|
+
if (needsDelegation) return { account: account2, service, needsDelegation: true };
|
|
10442
10677
|
const probe = PROBES[service];
|
|
10443
10678
|
return probe ? { account: account2, service, operation: probe.operation } : { account: account2, service };
|
|
10444
10679
|
}
|
|
10680
|
+
function skipDetail(target) {
|
|
10681
|
+
if (target.needsDelegation) {
|
|
10682
|
+
return `${target.service} needs domain-wide delegation, which a Workspace admin sets up; with it, add ${target.service} or ${target.service}.readonly to accounts.${target.account}.scopes and sign in again`;
|
|
10683
|
+
}
|
|
10684
|
+
return "no read-only check exists for this service";
|
|
10685
|
+
}
|
|
10445
10686
|
function probeArgv(target) {
|
|
10446
10687
|
const probe = PROBES[target.service];
|
|
10447
10688
|
const entry = probe ? commandIndex().methods.get(probe.operation) : void 0;
|
|
@@ -10452,14 +10693,17 @@ var DISABLED = /accessNotConfigured|SERVICE_DISABLED|has not been used in projec
|
|
|
10452
10693
|
function probeOutcome(target, result) {
|
|
10453
10694
|
const parsed = parseResult(result, { maxInlineBytes: 64 * 1024 });
|
|
10454
10695
|
if (parsed.ok) return { ...target, status: "OK" };
|
|
10455
|
-
|
|
10696
|
+
if (privilegeRefused(result, target.service, parsed)) {
|
|
10697
|
+
return { ...target, status: "ADMIN_PRIVILEGE_REQUIRED", detail: "Google accepted the sign-in, but the account lacks the Workspace admin privilege this service needs" };
|
|
10698
|
+
}
|
|
10699
|
+
const problem = authProblem(result, target.service, parsed);
|
|
10456
10700
|
if (problem === "AUTH_REQUIRED") return { ...target, status: problem, detail: "Google rejected the sign-in" };
|
|
10457
10701
|
if (problem === "AUTH_SCOPE_MISSING") return { ...target, status: problem, detail: "Google says the sign-in lacks a scope for this service" };
|
|
10458
10702
|
const error2 = apiError(parsed.data);
|
|
10459
|
-
const
|
|
10703
|
+
const text4 = `${error2?.message ?? ""}
|
|
10460
10704
|
${result.stderr.toString("utf8")}`;
|
|
10461
|
-
if (DISABLED.test(
|
|
10462
|
-
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]);
|
|
10463
10707
|
return { ...target, status: "API_DISABLED", detail: "the API is not enabled in the OAuth client's Google Cloud project", enableUrl };
|
|
10464
10708
|
}
|
|
10465
10709
|
if (PROBES[target.service]?.notFoundIsOk && (error2?.code === 404 || error2?.code === 400)) return { ...target, status: "OK" };
|
|
@@ -10546,15 +10790,15 @@ var Accounts = class {
|
|
|
10546
10790
|
mode: profile.mode,
|
|
10547
10791
|
services: profile.enabledServices,
|
|
10548
10792
|
...profile.mode === "interactive" ? { integrationID } : {},
|
|
10793
|
+
...delegationField(profile),
|
|
10549
10794
|
...signIn
|
|
10550
10795
|
};
|
|
10551
10796
|
})
|
|
10552
10797
|
);
|
|
10553
10798
|
return {
|
|
10554
10799
|
auditDegraded: this.runtime.audit.degraded,
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
...options.hostWarning ? { hostWarning: options.hostWarning } : {},
|
|
10800
|
+
version: options.pluginVersion,
|
|
10801
|
+
host: options.host ?? { version: null, range: HOST_RANGE, tested: false },
|
|
10558
10802
|
binary: { path: this.runtime.binary, version: this.runtime.version },
|
|
10559
10803
|
services: [...this.runtime.visibleServices()].sort(),
|
|
10560
10804
|
accounts
|
|
@@ -10579,9 +10823,15 @@ var Accounts = class {
|
|
|
10579
10823
|
this.runtime.assertActive(ctx.signal);
|
|
10580
10824
|
const env = buildChildEnv(profile, this.accountDir(account2), this.runtime.workDir, credential?.access);
|
|
10581
10825
|
report2(ctx, { status: "running", operation });
|
|
10582
|
-
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
|
+
}
|
|
10583
10833
|
if (credential) {
|
|
10584
|
-
const problem = authProblem(result);
|
|
10834
|
+
const problem = authProblem(result, serviceOf(operation));
|
|
10585
10835
|
if (problem) this.observeAuth(account2, { status: "needs_auth", code: problem, message: this.authMessage(account2, problem, options.scopes) });
|
|
10586
10836
|
else if (result.exitCode === 0 && options.dryRun !== true) this.observeAuth(account2, { status: "ok" });
|
|
10587
10837
|
}
|
|
@@ -10612,9 +10862,14 @@ var Accounts = class {
|
|
|
10612
10862
|
return credential;
|
|
10613
10863
|
}
|
|
10614
10864
|
scopeMissing(account2, profile, operation, accepted) {
|
|
10615
|
-
|
|
10865
|
+
const reason = restriction(operation);
|
|
10866
|
+
if (reason === "app-only") {
|
|
10616
10867
|
return `AUTH_SCOPE_MISSING: ${operation} needs a scope Google grants only to an app's own credentials (a Chat app or a Classroom add-on), never to a person's sign-in, so '${account2}' cannot run it. Next step: Tell the user this method cannot run with a Google sign-in, and use another method.`;
|
|
10617
10868
|
}
|
|
10869
|
+
if (reason === "domain-wide-delegation") {
|
|
10870
|
+
const named2 = accepted.map(shortScope).join(" or ");
|
|
10871
|
+
return `AUTH_SCOPE_MISSING: ${operation} needs ${named2}, which Google grants only through domain-wide delegation that a Workspace admin sets up, and the sign-in for '${account2}' has not granted it. Next step: Tell the user this method needs domain-wide delegation; once their admin has set it up, they add ${named2} to accounts.${account2}.scopes and sign in again.`;
|
|
10872
|
+
}
|
|
10618
10873
|
const read = signInScopes(profile, "read");
|
|
10619
10874
|
const named = accepted.slice(0, 5).map(shortScope).join(", ");
|
|
10620
10875
|
const connect2 = this.connect(account2);
|
|
@@ -10629,26 +10884,29 @@ var Accounts = class {
|
|
|
10629
10884
|
}
|
|
10630
10885
|
/** The plugin-owned per-account gws directory: its Discovery cache, and in credentials_file mode its token cache. */
|
|
10631
10886
|
accountDir(account2) {
|
|
10632
|
-
return ownedDir(
|
|
10887
|
+
return ownedDir(join7(this.runtime.paths.cacheDir, "accounts", this.integrationId(account2).replace(/^gws-/, "")));
|
|
10633
10888
|
}
|
|
10634
|
-
/** Remediation text for an account's sign-in and
|
|
10635
|
-
remediation(account2, scopes) {
|
|
10889
|
+
/** Remediation text for an account's sign-in, consent and admin privilege problems, by credential mode. */
|
|
10890
|
+
remediation(account2, scopes, service) {
|
|
10636
10891
|
const profile = this.runtime.config.accounts[account2];
|
|
10637
10892
|
const named = scopes && scopes.length > 0 ? scopes.slice(0, 5).map(shortScope).join(", ") : void 0;
|
|
10638
10893
|
const grant = named ? `include one of: ${named}` : "include the scope this operation needs";
|
|
10894
|
+
const admin = `a Workspace admin account that has ${privilegeName(service)}`;
|
|
10895
|
+
const remove = `remove ${service ?? "the service"} from accounts.${account2}.enabledServices`;
|
|
10639
10896
|
if (profile?.mode === "credentials_file") {
|
|
10640
10897
|
const source = profile.credentialsFileEnv ? `the credentials file named by ${profile.credentialsFileEnv}` : "the credentials file";
|
|
10641
|
-
return { signIn: `replace ${source}`, consent: `replace ${source} with one whose grant can ${grant}` };
|
|
10898
|
+
return { signIn: `replace ${source}`, consent: `replace ${source} with one whose grant can ${grant}`, privilege: `replace ${source} with one for ${admin}, or ${remove}` };
|
|
10642
10899
|
}
|
|
10643
10900
|
if (profile?.mode === "access_token") {
|
|
10644
10901
|
const source = profile.tokenEnv ? `the access token in ${profile.tokenEnv}` : "the access token";
|
|
10645
|
-
return { signIn: `refresh ${source}`, consent: `mint ${source} with scopes that ${grant}` };
|
|
10902
|
+
return { signIn: `refresh ${source}`, consent: `mint ${source} with scopes that ${grant}`, privilege: `mint ${source} for ${admin}, or ${remove}` };
|
|
10646
10903
|
}
|
|
10647
10904
|
const connect2 = this.connect(account2);
|
|
10648
10905
|
const extra = named ? `, or add one of ${named} to accounts.${account2}.scopes` : "";
|
|
10649
10906
|
return {
|
|
10650
10907
|
signIn: `to sign in again, ${connect2}`,
|
|
10651
|
-
consent: `to grant the scope, ${connect2}, sign in again, and approve every scope, choosing Read and write for a change${extra}
|
|
10908
|
+
consent: `to grant the scope, ${connect2}, sign in again, and approve every scope, choosing Read and write for a change${extra}`,
|
|
10909
|
+
privilege: `sign in with ${admin} (${connect2}), or ${remove}`
|
|
10652
10910
|
};
|
|
10653
10911
|
}
|
|
10654
10912
|
authMessage(account2, code2, scopes) {
|
|
@@ -10667,11 +10925,13 @@ var Accounts = class {
|
|
|
10667
10925
|
/** Every account and enabled service /gws-doctor checks, in config order. */
|
|
10668
10926
|
doctorTargets() {
|
|
10669
10927
|
this.runtime.assertActive();
|
|
10670
|
-
return Object.entries(this.runtime.config.accounts).flatMap(
|
|
10928
|
+
return Object.entries(this.runtime.config.accounts).flatMap(
|
|
10929
|
+
([alias, profile]) => profile.enabledServices.map((service) => doctorTarget(alias, service, profile.mode === "interactive" && delegationRequired([service], profile.scopes).length > 0))
|
|
10930
|
+
);
|
|
10671
10931
|
}
|
|
10672
10932
|
/** Run one /gws-doctor probe as the account. A failure becomes an outcome, never a throw, except cancellation. */
|
|
10673
10933
|
async probe(target, ctx) {
|
|
10674
|
-
if (!target.operation) return { ...target, status: "SKIPPED", detail:
|
|
10934
|
+
if (!target.operation) return { ...target, status: "SKIPPED", detail: skipDetail(target) };
|
|
10675
10935
|
const started = Date.now();
|
|
10676
10936
|
try {
|
|
10677
10937
|
const result = await this.run(probeArgv(target), target.account, target.operation, ctx);
|
|
@@ -10712,16 +10972,25 @@ function report2(ctx, update) {
|
|
|
10712
10972
|
} catch {
|
|
10713
10973
|
}
|
|
10714
10974
|
}
|
|
10715
|
-
function
|
|
10716
|
-
|
|
10975
|
+
function delegationField(profile) {
|
|
10976
|
+
if (profile.mode !== "interactive") return {};
|
|
10977
|
+
const services = delegationRequired(profile.enabledServices, profile.scopes);
|
|
10978
|
+
return services.length > 0 ? { delegationRequired: services } : {};
|
|
10979
|
+
}
|
|
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);
|
|
10717
10985
|
}
|
|
10718
10986
|
|
|
10719
10987
|
// src/domain/classify.ts
|
|
10720
|
-
var NOTIFYING_UPDATES = /* @__PURE__ */ new Set(["all", "externalonly"]);
|
|
10721
10988
|
function emailsAttendees(entry, request) {
|
|
10722
10989
|
if (entry.service !== "calendar" || entry.path.join(".") !== "events") return false;
|
|
10723
|
-
const
|
|
10724
|
-
|
|
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");
|
|
10725
10994
|
}
|
|
10726
10995
|
function effectiveCategory(entry, request) {
|
|
10727
10996
|
if (entry.category === "deny" || entry.schemaError) return "deny";
|
|
@@ -10737,27 +11006,390 @@ function isCommittable(category) {
|
|
|
10737
11006
|
|
|
10738
11007
|
// src/domain/command.ts
|
|
10739
11008
|
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
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) {
|
|
10743
11035
|
if (typeof value === "string") {
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
}
|
|
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);
|
|
10747
11069
|
return;
|
|
10748
11070
|
}
|
|
10749
11071
|
if (Array.isArray(value)) {
|
|
10750
|
-
for (const item of value) visit(item,
|
|
11072
|
+
for (const item of value) visit(item, collecting);
|
|
10751
11073
|
return;
|
|
10752
11074
|
}
|
|
10753
11075
|
if (value && typeof value === "object") {
|
|
10754
|
-
for (const [
|
|
11076
|
+
for (const [key, child] of Object.entries(value)) visit(child, collecting || RECIPIENT_KEYS.has(key.toLowerCase()));
|
|
11077
|
+
}
|
|
11078
|
+
};
|
|
11079
|
+
visit(request.params, false);
|
|
11080
|
+
visit(request.body, false);
|
|
11081
|
+
visit(request.input, false);
|
|
11082
|
+
return out.result();
|
|
11083
|
+
}
|
|
11084
|
+
function record2(value) {
|
|
11085
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11086
|
+
}
|
|
11087
|
+
function text3(value) {
|
|
11088
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
11089
|
+
}
|
|
11090
|
+
function mimeRecipients(raw) {
|
|
11091
|
+
if (typeof raw !== "string" || raw.length === 0 || !/^[A-Za-z0-9+/_=\s-]+$/.test(raw)) return void 0;
|
|
11092
|
+
const message = Buffer.from(raw.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
11093
|
+
const end = /\r?\n\r?\n/.exec(message);
|
|
11094
|
+
const head = (end ? message.slice(0, end.index) : message).replace(/\r?\n[ \t]+/g, " ");
|
|
11095
|
+
const lines = head.split(/\r?\n/);
|
|
11096
|
+
if (!lines.every((line) => line === "" || /^[!-9;-~]+[ \t]*:/.test(line))) return void 0;
|
|
11097
|
+
return lines.filter((line) => /^(?:to|cc|bcc)[ \t]*:/i.test(line)).map((line) => line.slice(line.indexOf(":") + 1));
|
|
11098
|
+
}
|
|
11099
|
+
function headers(message) {
|
|
11100
|
+
const list = record2(record2(message)["payload"])["headers"];
|
|
11101
|
+
if (!Array.isArray(list)) return void 0;
|
|
11102
|
+
const out = /* @__PURE__ */ new Map();
|
|
11103
|
+
for (const header of list) {
|
|
11104
|
+
const name = text3(record2(header)["name"])?.toLowerCase();
|
|
11105
|
+
const value = record2(header)["value"];
|
|
11106
|
+
if (name && typeof value === "string") out.set(name, [...out.get(name) ?? [], value]);
|
|
11107
|
+
}
|
|
11108
|
+
return out;
|
|
11109
|
+
}
|
|
11110
|
+
function setOf(values) {
|
|
11111
|
+
const out = new Collector();
|
|
11112
|
+
out.text(values);
|
|
11113
|
+
return out.unknown ? void 0 : { addresses: [...out.addresses], domains: [] };
|
|
11114
|
+
}
|
|
11115
|
+
function plan(out, lookups = [], needsSelf = false) {
|
|
11116
|
+
return { known: out.result(), lookups, ...needsSelf ? { needsSelf: true } : {} };
|
|
11117
|
+
}
|
|
11118
|
+
function helperSend(fields) {
|
|
11119
|
+
return (request) => {
|
|
11120
|
+
const input = record2(request.input);
|
|
11121
|
+
const out = new Collector();
|
|
11122
|
+
for (const field of fields) out.text(input[field]);
|
|
11123
|
+
if (out.addresses.size === 0) out.fail("no recipient address was found");
|
|
11124
|
+
return plan(out);
|
|
11125
|
+
};
|
|
11126
|
+
}
|
|
11127
|
+
function replySpec(all) {
|
|
11128
|
+
return (request) => {
|
|
11129
|
+
const input = record2(request.input);
|
|
11130
|
+
const out = new Collector();
|
|
11131
|
+
for (const field of ["to", "cc", "bcc"]) out.text(input[field]);
|
|
11132
|
+
const removed = new Set(addressesIn([input["remove"]].flat().filter((item) => typeof item === "string").join(",")).addresses);
|
|
11133
|
+
const messageId = text3(input["messageId"]);
|
|
11134
|
+
if (!messageId) {
|
|
11135
|
+
out.fail("the message being replied to is not named");
|
|
11136
|
+
return plan(out);
|
|
11137
|
+
}
|
|
11138
|
+
const lookup = {
|
|
11139
|
+
operation: "gmail.users.messages.get",
|
|
11140
|
+
params: { userId: "me", id: messageId, format: "metadata", fields: "payload/headers" },
|
|
11141
|
+
what: "the message being replied to",
|
|
11142
|
+
extract(data, self) {
|
|
11143
|
+
const found = headers(data);
|
|
11144
|
+
if (!found) return void 0;
|
|
11145
|
+
const sender = found.get("reply-to") ?? found.get("from") ?? [];
|
|
11146
|
+
const senderSet = setOf(sender);
|
|
11147
|
+
if (!senderSet) return void 0;
|
|
11148
|
+
const values = [...sender];
|
|
11149
|
+
if (all || self !== void 0 && senderSet.addresses.includes(self)) values.push(...found.get("to") ?? []);
|
|
11150
|
+
if (all) values.push(...found.get("cc") ?? []);
|
|
11151
|
+
const set = setOf(values);
|
|
11152
|
+
if (!set) return void 0;
|
|
11153
|
+
const kept = all ? set.addresses.filter((address) => address !== self && !removed.has(address)) : set.addresses;
|
|
11154
|
+
return { addresses: kept, domains: [] };
|
|
11155
|
+
}
|
|
11156
|
+
};
|
|
11157
|
+
return plan(out, [lookup], true);
|
|
11158
|
+
};
|
|
11159
|
+
}
|
|
11160
|
+
function rawInto(out, raw) {
|
|
11161
|
+
const values = mimeRecipients(raw);
|
|
11162
|
+
if (values) out.text(values);
|
|
11163
|
+
else out.fail("the raw message has no readable header block");
|
|
11164
|
+
}
|
|
11165
|
+
var messagesSend = (request) => {
|
|
11166
|
+
const out = new Collector();
|
|
11167
|
+
if (request.upload) out.fail("the message is in an uploaded file");
|
|
11168
|
+
else if (record2(request.body)["raw"] === void 0) out.fail("the message has no raw field");
|
|
11169
|
+
else rawInto(out, record2(request.body)["raw"]);
|
|
11170
|
+
if (!out.unknown && out.addresses.size === 0) out.fail("the raw message has no To, Cc or Bcc header");
|
|
11171
|
+
return plan(out);
|
|
11172
|
+
};
|
|
11173
|
+
var draftsSend = (request) => {
|
|
11174
|
+
const out = new Collector();
|
|
11175
|
+
const body = record2(request.body);
|
|
11176
|
+
const raw = record2(body["message"])["raw"];
|
|
11177
|
+
if (raw !== void 0) rawInto(out, raw);
|
|
11178
|
+
const id = text3(body["id"]);
|
|
11179
|
+
if (!id) {
|
|
11180
|
+
out.fail("the draft is not named (body.id)");
|
|
11181
|
+
return plan(out);
|
|
11182
|
+
}
|
|
11183
|
+
const userId = text3(record2(request.params)["userId"]) ?? "me";
|
|
11184
|
+
const lookup = {
|
|
11185
|
+
operation: "gmail.users.drafts.get",
|
|
11186
|
+
params: { userId, id, format: "metadata", fields: "message/payload/headers" },
|
|
11187
|
+
what: "the draft",
|
|
11188
|
+
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;
|
|
10755
11233
|
}
|
|
10756
11234
|
};
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
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
|
+
};
|
|
10760
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
|
|
10761
11393
|
function wantsPages(request) {
|
|
10762
11394
|
const pagination = request.pagination ?? {};
|
|
10763
11395
|
return pagination.all === true || pagination.maxPages !== void 0 || pagination.maxItems !== void 0;
|
|
@@ -10836,7 +11468,7 @@ function buildCommand(entry, request, config2, options) {
|
|
|
10836
11468
|
url: entry.pathTemplate,
|
|
10837
11469
|
params: redactParams(params),
|
|
10838
11470
|
bodySummary: request.body === void 0 ? void 0 : summarizeBody(request.body),
|
|
10839
|
-
recipients:
|
|
11471
|
+
recipients: recipientLabels(scanRecipients(request)),
|
|
10840
11472
|
uploads: request.upload ? [request.upload.path] : void 0,
|
|
10841
11473
|
destination: request.download?.path,
|
|
10842
11474
|
overwrite: request.download?.overwrite === true ? true : void 0,
|
|
@@ -10874,32 +11506,6 @@ function matches2(rule, category, account2, service, operation) {
|
|
|
10874
11506
|
function underRoot2(path, roots) {
|
|
10875
11507
|
return roots.some((root) => underRoot(path, root));
|
|
10876
11508
|
}
|
|
10877
|
-
function collectEmails(value, out) {
|
|
10878
|
-
if (typeof value === "string") {
|
|
10879
|
-
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) out.add(value.toLowerCase());
|
|
10880
|
-
return;
|
|
10881
|
-
}
|
|
10882
|
-
if (Array.isArray(value)) {
|
|
10883
|
-
for (const item of value) collectEmails(item, out);
|
|
10884
|
-
return;
|
|
10885
|
-
}
|
|
10886
|
-
if (value && typeof value === "object") {
|
|
10887
|
-
for (const [key, item] of Object.entries(value)) {
|
|
10888
|
-
if (key === "email" || key === "emailAddress" || key === "address" || key === "to" || key === "cc" || key === "bcc" || key === "recipient" || key === "attendees") {
|
|
10889
|
-
collectEmails(item, out);
|
|
10890
|
-
} else if (typeof item === "object") {
|
|
10891
|
-
collectEmails(item, out);
|
|
10892
|
-
}
|
|
10893
|
-
}
|
|
10894
|
-
}
|
|
10895
|
-
}
|
|
10896
|
-
function emails2(request) {
|
|
10897
|
-
const out = /* @__PURE__ */ new Set();
|
|
10898
|
-
collectEmails(request.params, out);
|
|
10899
|
-
collectEmails(request.body, out);
|
|
10900
|
-
collectEmails(request.input, out);
|
|
10901
|
-
return [...out];
|
|
10902
|
-
}
|
|
10903
11509
|
function helperPaths(request, keys) {
|
|
10904
11510
|
const input = request.input;
|
|
10905
11511
|
if (!input || typeof input !== "object") return [];
|
|
@@ -10917,13 +11523,19 @@ function publicSharing(request) {
|
|
|
10917
11523
|
const haystack = JSON.stringify({ params: request.params ?? {}, body: request.body ?? {}, input: request.input ?? {} }).toLowerCase();
|
|
10918
11524
|
return /"role"\s*:\s*"(anyone|public)"/.test(haystack) || /"type"\s*:\s*"(anyone|public)"/.test(haystack);
|
|
10919
11525
|
}
|
|
10920
|
-
function
|
|
10921
|
-
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);
|
|
10922
11534
|
}
|
|
10923
11535
|
function constrained(rule) {
|
|
10924
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;
|
|
10925
11537
|
}
|
|
10926
|
-
function evaluatePolicy(config2, entry, category, request) {
|
|
11538
|
+
function evaluatePolicy(config2, entry, category, request, facts) {
|
|
10927
11539
|
const constraints = {
|
|
10928
11540
|
maxPages: config2.execution.maxPages,
|
|
10929
11541
|
maxItems: config2.execution.maxItems,
|
|
@@ -10933,31 +11545,21 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
10933
11545
|
writeRoots: config2.execution.allowedWriteRoots.length > 0 ? [config2.execution.allowedWriteRoots] : []
|
|
10934
11546
|
};
|
|
10935
11547
|
const violations = [];
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
if (!config2.accounts[request.account]) {
|
|
10940
|
-
return { effect: "deny", violations: ["account-unknown"], constraints };
|
|
10941
|
-
}
|
|
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");
|
|
10942
11551
|
const profile = config2.accounts[request.account];
|
|
10943
11552
|
const service = entry.service;
|
|
10944
|
-
if (!profile.enabledServices.includes(service))
|
|
10945
|
-
|
|
10946
|
-
}
|
|
10947
|
-
if (category === "deploy" || category === "watch" || category === "deny") {
|
|
10948
|
-
return { effect: "deny", violations: ["never-committable"], constraints };
|
|
10949
|
-
}
|
|
11553
|
+
if (!profile.enabledServices.includes(service)) return refuse("service-disabled");
|
|
11554
|
+
if (category === "deploy" || category === "watch" || category === "deny") return refuse("never-committable");
|
|
10950
11555
|
if (category === "admin") {
|
|
10951
|
-
if (!config2.admin.enabled)
|
|
10952
|
-
|
|
10953
|
-
}
|
|
10954
|
-
if (!config2.admin.allowedServices.includes(service)) {
|
|
10955
|
-
return { effect: "deny", violations: ["admin-service-not-allowed"], constraints };
|
|
10956
|
-
}
|
|
11556
|
+
if (!config2.admin.enabled) return refuse("admin-disabled");
|
|
11557
|
+
if (!config2.admin.allowedServices.includes(service)) return refuse("admin-service-not-allowed");
|
|
10957
11558
|
}
|
|
10958
11559
|
let denied2 = false;
|
|
10959
11560
|
let ruleId;
|
|
10960
|
-
|
|
11561
|
+
let unknownRecipients = 0;
|
|
11562
|
+
let unknownDomain = 0;
|
|
10961
11563
|
const readPaths = [
|
|
10962
11564
|
...request.upload?.path ? [request.upload.path] : [],
|
|
10963
11565
|
...helperPaths(request, ["path", "file", "filePath", "source", "attachments"])
|
|
@@ -10966,28 +11568,44 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
10966
11568
|
...request.download?.path ? [request.download.path] : [],
|
|
10967
11569
|
...helperPaths(request, ["destination", "output", "outPath"])
|
|
10968
11570
|
];
|
|
10969
|
-
const
|
|
10970
|
-
const
|
|
10971
|
-
|
|
10972
|
-
|
|
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)) {
|
|
10973
11579
|
const before = violations.length;
|
|
10974
|
-
if (
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
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
|
+
}
|
|
10991
11609
|
}
|
|
10992
11610
|
if (rule.allowPublicSharing === false && isPublic) {
|
|
10993
11611
|
violations.push("public sharing is disabled");
|
|
@@ -11025,16 +11643,18 @@ function evaluatePolicy(config2, entry, category, request) {
|
|
|
11025
11643
|
violations.push("admin plans require a non-empty reason");
|
|
11026
11644
|
}
|
|
11027
11645
|
if (violations.length > 0 || denied2) {
|
|
11028
|
-
|
|
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 };
|
|
11029
11649
|
}
|
|
11030
|
-
return { effect: "allow", ruleId, violations, constraints };
|
|
11650
|
+
return { effect: "allow", ruleId, violations, constraints, code: "POLICY_DENIED" };
|
|
11031
11651
|
}
|
|
11032
11652
|
|
|
11033
11653
|
// src/process/staging.ts
|
|
11034
11654
|
import { createHash as createHash3 } from "node:crypto";
|
|
11035
11655
|
import { constants } from "node:fs";
|
|
11036
|
-
import { link, lstat, mkdir as mkdir2, mkdtemp, open as open2, realpath, rename, rm, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
11037
|
-
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";
|
|
11038
11658
|
var CHUNK = 64 * 1024;
|
|
11039
11659
|
var READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
11040
11660
|
var Stage = class _Stage {
|
|
@@ -11046,13 +11666,13 @@ var Stage = class _Stage {
|
|
|
11046
11666
|
signal;
|
|
11047
11667
|
static async create(workDir, signal) {
|
|
11048
11668
|
throwIfCancelled(signal);
|
|
11049
|
-
const dir = await
|
|
11669
|
+
const dir = await mkdtemp2(join8(workDir, "call-"));
|
|
11050
11670
|
try {
|
|
11051
11671
|
throwIfCancelled(signal);
|
|
11052
|
-
await writeFile2(
|
|
11672
|
+
await writeFile2(join8(dir, ".env"), "", { mode: 384 });
|
|
11053
11673
|
return new _Stage(dir, signal);
|
|
11054
11674
|
} catch (error2) {
|
|
11055
|
-
await
|
|
11675
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
11056
11676
|
throw error2;
|
|
11057
11677
|
}
|
|
11058
11678
|
}
|
|
@@ -11066,7 +11686,7 @@ var Stage = class _Stage {
|
|
|
11066
11686
|
/** Create the directory gws writes the output into. */
|
|
11067
11687
|
async prepareOutput(output) {
|
|
11068
11688
|
throwIfCancelled(this.signal);
|
|
11069
|
-
await mkdir2(dirname5(
|
|
11689
|
+
await mkdir2(dirname5(join8(this.dir, output.rel)), { recursive: true, mode: 448 });
|
|
11070
11690
|
}
|
|
11071
11691
|
/**
|
|
11072
11692
|
* Move the staged output to its destination: size cap, then the same
|
|
@@ -11076,7 +11696,7 @@ var Stage = class _Stage {
|
|
|
11076
11696
|
*/
|
|
11077
11697
|
async deliver(output, limits) {
|
|
11078
11698
|
throwIfCancelled(this.signal);
|
|
11079
|
-
const staged =
|
|
11699
|
+
const staged = join8(this.dir, output.rel);
|
|
11080
11700
|
const info = await lstat(staged).catch(() => void 0);
|
|
11081
11701
|
if (!info?.isFile()) throw new GwsError("DOWNLOAD_INVALID", "gws did not write the output file", { next: "Report it; the plan is used up." });
|
|
11082
11702
|
if (info.size > limits.maxDownloadBytes) {
|
|
@@ -11088,7 +11708,7 @@ var Stage = class _Stage {
|
|
|
11088
11708
|
return { path: output.destination, bytes: info.size, sha256, ...cleanupDegraded ? { cleanupDegraded: true } : {} };
|
|
11089
11709
|
}
|
|
11090
11710
|
async dispose() {
|
|
11091
|
-
await
|
|
11711
|
+
await rm2(this.dir, { recursive: true, force: true });
|
|
11092
11712
|
}
|
|
11093
11713
|
async stageInput(input, maxBytes, roots) {
|
|
11094
11714
|
throwIfCancelled(this.signal);
|
|
@@ -11100,7 +11720,7 @@ var Stage = class _Stage {
|
|
|
11100
11720
|
next: "Use a file inside execution.allowedReadRoots, or ask the user to add its folder there."
|
|
11101
11721
|
});
|
|
11102
11722
|
}
|
|
11103
|
-
if (!(await
|
|
11723
|
+
if (!(await stat2(real)).isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11104
11724
|
let source;
|
|
11105
11725
|
let target;
|
|
11106
11726
|
try {
|
|
@@ -11108,7 +11728,7 @@ var Stage = class _Stage {
|
|
|
11108
11728
|
const info = await source.stat();
|
|
11109
11729
|
if (!info.isFile()) throw new GwsError("UPLOAD_INVALID", `'${input.source}' is not a regular file`);
|
|
11110
11730
|
if (info.size > maxBytes) throw uploadLimit(input.source, info.size, maxBytes);
|
|
11111
|
-
const destination =
|
|
11731
|
+
const destination = join8(this.dir, input.rel);
|
|
11112
11732
|
throwIfCancelled(this.signal);
|
|
11113
11733
|
await mkdir2(dirname5(destination), { recursive: true, mode: 448 });
|
|
11114
11734
|
throwIfCancelled(this.signal);
|
|
@@ -11138,7 +11758,7 @@ async function checkDestination(output, limits) {
|
|
|
11138
11758
|
if (!isAbsolute7(output.destination)) throw new GwsError("DOWNLOAD_INVALID", `'${output.destination}' must be an absolute path`);
|
|
11139
11759
|
const parent = await realpath(dirname5(output.destination)).catch(() => void 0);
|
|
11140
11760
|
if (!parent) throw new GwsError("DOWNLOAD_INVALID", `the directory for '${output.destination}' does not exist`);
|
|
11141
|
-
const target =
|
|
11761
|
+
const target = join8(parent, basename2(output.destination));
|
|
11142
11762
|
if (!withinRootSets(target, await realRootSets(limits.writeRoots))) {
|
|
11143
11763
|
throw new GwsError("POLICY_DENIED", `download path '${output.destination}' resolves outside allowed write roots`, {
|
|
11144
11764
|
next: "Use a destination inside execution.allowedWriteRoots, or ask the user to add its folder there."
|
|
@@ -11152,20 +11772,20 @@ async function checkDestination(output, limits) {
|
|
|
11152
11772
|
async function place(staged, target, output, signal) {
|
|
11153
11773
|
throwIfCancelled(signal);
|
|
11154
11774
|
try {
|
|
11155
|
-
if (output.overwrite) await
|
|
11775
|
+
if (output.overwrite) await rename2(staged, target);
|
|
11156
11776
|
else await link(staged, target);
|
|
11157
11777
|
return false;
|
|
11158
11778
|
} catch (error2) {
|
|
11159
11779
|
if (code(error2) === "EEXIST") throw exists(output.destination);
|
|
11160
11780
|
if (code(error2) !== "EXDEV" && code(error2) !== "EPERM" && code(error2) !== "ENOTSUP") throw error2;
|
|
11161
11781
|
}
|
|
11162
|
-
const temp =
|
|
11782
|
+
const temp = join8(dirname5(target), `.${basename2(target)}.${process.pid}.${Date.now()}.partial`);
|
|
11163
11783
|
let cleanupDegraded = false;
|
|
11164
11784
|
try {
|
|
11165
11785
|
throwIfCancelled(signal);
|
|
11166
11786
|
await copyDurable(staged, temp, signal);
|
|
11167
11787
|
throwIfCancelled(signal);
|
|
11168
|
-
if (output.overwrite) await
|
|
11788
|
+
if (output.overwrite) await rename2(temp, target);
|
|
11169
11789
|
else {
|
|
11170
11790
|
try {
|
|
11171
11791
|
await link(temp, target);
|
|
@@ -11234,6 +11854,17 @@ function code(error2) {
|
|
|
11234
11854
|
// src/runtime/operations.ts
|
|
11235
11855
|
var METHOD_FIELDS = ["params", "body", "fields", "pagination", "upload", "download"];
|
|
11236
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
|
+
}
|
|
11237
11868
|
var Operations = class {
|
|
11238
11869
|
constructor(runtime, accounts) {
|
|
11239
11870
|
this.runtime = runtime;
|
|
@@ -11241,6 +11872,8 @@ var Operations = class {
|
|
|
11241
11872
|
}
|
|
11242
11873
|
runtime;
|
|
11243
11874
|
accounts;
|
|
11875
|
+
/** Each account's own address, once read. */
|
|
11876
|
+
addresses = /* @__PURE__ */ new Map();
|
|
11244
11877
|
/** The configured account a call names, or the only one when it names none. */
|
|
11245
11878
|
account(alias) {
|
|
11246
11879
|
const aliases = Object.keys(this.runtime.config.accounts);
|
|
@@ -11312,8 +11945,9 @@ var Operations = class {
|
|
|
11312
11945
|
throw new GwsError("POLICY_DENIED", `'${entry.id}' is ${category2}; use gws_plan and the matching commit tool`, { next: PLAN_INSTEAD });
|
|
11313
11946
|
}
|
|
11314
11947
|
}
|
|
11315
|
-
const
|
|
11316
|
-
|
|
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);
|
|
11317
11951
|
return { category: "read", decision: decision2 };
|
|
11318
11952
|
}
|
|
11319
11953
|
const category = helper ? helper.category : effectiveCategory(entry, request);
|
|
@@ -11322,8 +11956,8 @@ var Operations = class {
|
|
|
11322
11956
|
next: category === "read" ? "Run it with gws_read." : "Tell the user the plugin never runs it."
|
|
11323
11957
|
});
|
|
11324
11958
|
}
|
|
11325
|
-
const decision = evaluatePolicy(this.runtime.config, entry, category, request);
|
|
11326
|
-
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);
|
|
11327
11961
|
if (category === "admin" && (!this.runtime.config.admin.enabled || decision.effect !== "allow")) {
|
|
11328
11962
|
throw new GwsError("POLICY_DENIED", "admin operations require admin.enabled, an allowed service, and a reason when one is required");
|
|
11329
11963
|
}
|
|
@@ -11336,11 +11970,13 @@ var Operations = class {
|
|
|
11336
11970
|
inspect(input, mode) {
|
|
11337
11971
|
const resolved = this.resolve(input);
|
|
11338
11972
|
const { category } = this.gate(resolved, mode);
|
|
11973
|
+
const reads = mode === "plan" ? recipientPlan(resolved.entry.id, category, resolved.request).lookups.map((lookup) => lookup.operation) : [];
|
|
11339
11974
|
return {
|
|
11340
11975
|
account: resolved.request.account,
|
|
11341
11976
|
operation: resolved.entry.id,
|
|
11342
11977
|
service: resolved.entry.service,
|
|
11343
|
-
category
|
|
11978
|
+
category,
|
|
11979
|
+
...reads.length > 0 ? { reads } : {}
|
|
11344
11980
|
};
|
|
11345
11981
|
}
|
|
11346
11982
|
/**
|
|
@@ -11371,7 +12007,7 @@ var Operations = class {
|
|
|
11371
12007
|
report2(ctx, { status: "running", operation: entry.id });
|
|
11372
12008
|
const schema = await this.runtime.schema.resolve(entry.service, [...entry.path, entry.method], ctx.signal);
|
|
11373
12009
|
this.runtime.assertActive(ctx.signal);
|
|
11374
|
-
return { ok: true, account: target.account, schema, ...
|
|
12010
|
+
return { ok: true, account: target.account, schema, ...restrictionMarks(entry.id) };
|
|
11375
12011
|
}
|
|
11376
12012
|
/** Validate a commit against its stored plan without consuming it. */
|
|
11377
12013
|
inspectPlan(category, input, ctx) {
|
|
@@ -11396,7 +12032,7 @@ var Operations = class {
|
|
|
11396
12032
|
requestedInlineBytes: helper ? void 0 : request.response?.maxInlineBytes
|
|
11397
12033
|
});
|
|
11398
12034
|
const failed = helper ? result.exitCode !== 0 || isApiError(parsed.data) : !parsed.ok;
|
|
11399
|
-
if (failed) throw processError(result, parsed, this.accounts.remediation(request.account, entry.scopes));
|
|
12035
|
+
if (failed) throw processError(result, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11400
12036
|
const pages = paged ? capPageItems(parsed.data, itemCap(request, limits)) : void 0;
|
|
11401
12037
|
const cap = inlineCap(this.runtime.config.execution.maxInlineBytes, request.response?.mode, request.response?.maxInlineBytes);
|
|
11402
12038
|
const truncated = pages ? Buffer.byteLength(JSON.stringify(pages.data)) > cap : parsed.truncated;
|
|
@@ -11413,7 +12049,7 @@ var Operations = class {
|
|
|
11413
12049
|
ok: true,
|
|
11414
12050
|
operation: entry.id,
|
|
11415
12051
|
category: "read",
|
|
11416
|
-
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),
|
|
11417
12053
|
truncated,
|
|
11418
12054
|
diagnostics: parsed.diagnostics,
|
|
11419
12055
|
pagination: pages ? { pages: pages.pages, items: pages.items, capped: pages.capped } : void 0,
|
|
@@ -11428,8 +12064,12 @@ var Operations = class {
|
|
|
11428
12064
|
const resolved = this.resolve(input);
|
|
11429
12065
|
const { entry, helper, request } = resolved;
|
|
11430
12066
|
const started = Date.now();
|
|
11431
|
-
const { category
|
|
12067
|
+
const { category } = this.gate(resolved, "plan");
|
|
11432
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);
|
|
11433
12073
|
const limits = fileLimits(decision);
|
|
11434
12074
|
let dryRunArgv;
|
|
11435
12075
|
let commitArgv;
|
|
@@ -11460,14 +12100,16 @@ var Operations = class {
|
|
|
11460
12100
|
const dry = await this.accounts.run(dryRunArgv, request.account, entry.id, ctx, { scopes: entry.scopes, dryRun: true, cwd: stage?.dir });
|
|
11461
12101
|
report2(ctx, { status: "parsing", operation: entry.id });
|
|
11462
12102
|
parsed = parseResult(dry, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
11463
|
-
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, this.accounts.remediation(request.account, entry.scopes));
|
|
12103
|
+
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, entry.service, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11464
12104
|
} finally {
|
|
11465
12105
|
await stage?.dispose();
|
|
11466
12106
|
}
|
|
11467
12107
|
throwIfCancelled(ctx.signal);
|
|
11468
12108
|
const reason = request.reason?.trim();
|
|
11469
|
-
const
|
|
11470
|
-
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({
|
|
11471
12113
|
account: request.account,
|
|
11472
12114
|
sessionId: ctx.sessionId,
|
|
11473
12115
|
agentId: ctx.agentId,
|
|
@@ -11477,6 +12119,8 @@ var Operations = class {
|
|
|
11477
12119
|
argv: commitArgv,
|
|
11478
12120
|
preview: planPreview,
|
|
11479
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) } } : {},
|
|
11480
12124
|
ttlSeconds: this.runtime.config.policy.planTtlSeconds
|
|
11481
12125
|
});
|
|
11482
12126
|
this.runtime.audit.emit({
|
|
@@ -11490,8 +12134,8 @@ var Operations = class {
|
|
|
11490
12134
|
});
|
|
11491
12135
|
return {
|
|
11492
12136
|
ok: true,
|
|
11493
|
-
planId:
|
|
11494
|
-
expiresAt: new Date(
|
|
12137
|
+
planId: plan2.id,
|
|
12138
|
+
expiresAt: new Date(plan2.expiresAt).toISOString(),
|
|
11495
12139
|
category: commitCategory,
|
|
11496
12140
|
preview: planPreview,
|
|
11497
12141
|
dryRun: redactDryRun(parsed.data)
|
|
@@ -11502,7 +12146,7 @@ var Operations = class {
|
|
|
11502
12146
|
throwIfCancelled(ctx.signal);
|
|
11503
12147
|
if (existing.files?.output) await checkDestination(existing.files.output, existing.files.limits);
|
|
11504
12148
|
this.runtime.assertActive(ctx.signal);
|
|
11505
|
-
const
|
|
12149
|
+
const plan2 = this.runtime.plans.acquire(input.planId, {
|
|
11506
12150
|
sessionId: ctx.sessionId,
|
|
11507
12151
|
agentId: ctx.agentId,
|
|
11508
12152
|
account: existing.account,
|
|
@@ -11512,38 +12156,41 @@ var Operations = class {
|
|
|
11512
12156
|
let stage;
|
|
11513
12157
|
let receipt;
|
|
11514
12158
|
try {
|
|
11515
|
-
if (
|
|
11516
|
-
if (stage
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
const
|
|
11523
|
-
|
|
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);
|
|
12167
|
+
const scopes = method?.scopes;
|
|
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 });
|
|
11524
12170
|
const parsed = parseResult(result, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
11525
|
-
if (!parsed.ok) throw processError(result, parsed, this.accounts.remediation(
|
|
11526
|
-
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;
|
|
11527
12173
|
const file = delivered ? { path: delivered.path, bytes: delivered.bytes, sha256: delivered.sha256 } : void 0;
|
|
11528
|
-
this.runtime.plans.complete(
|
|
12174
|
+
this.runtime.plans.complete(plan2.id);
|
|
11529
12175
|
this.runtime.audit.emit({
|
|
11530
12176
|
event: "operation.commit",
|
|
11531
12177
|
outcome: "success",
|
|
11532
|
-
account:
|
|
12178
|
+
account: plan2.account,
|
|
11533
12179
|
category,
|
|
11534
|
-
operation:
|
|
12180
|
+
operation: plan2.operation,
|
|
11535
12181
|
durationMs: Date.now() - started
|
|
11536
12182
|
});
|
|
11537
|
-
|
|
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 } : {} };
|
|
11538
12185
|
return receipt;
|
|
11539
12186
|
} catch (error2) {
|
|
11540
|
-
this.runtime.plans.release(
|
|
12187
|
+
this.runtime.plans.release(plan2.id);
|
|
11541
12188
|
this.runtime.audit.emit({
|
|
11542
12189
|
event: "operation.commit",
|
|
11543
12190
|
outcome: "error",
|
|
11544
|
-
account:
|
|
12191
|
+
account: plan2.account,
|
|
11545
12192
|
category,
|
|
11546
|
-
operation:
|
|
12193
|
+
operation: plan2.operation,
|
|
11547
12194
|
durationMs: Date.now() - started,
|
|
11548
12195
|
errorCode: auditErrorCode(error2)
|
|
11549
12196
|
});
|
|
@@ -11563,6 +12210,85 @@ var Operations = class {
|
|
|
11563
12210
|
}
|
|
11564
12211
|
}
|
|
11565
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
|
+
}
|
|
11566
12292
|
};
|
|
11567
12293
|
function fileLimits(decision) {
|
|
11568
12294
|
const { maxUploadBytes, maxDownloadBytes, readRoots, writeRoots } = decision.constraints;
|
|
@@ -11597,10 +12323,9 @@ var Runtime = class _Runtime {
|
|
|
11597
12323
|
this.version = version2;
|
|
11598
12324
|
this.binary = binary;
|
|
11599
12325
|
this.paths = options.paths ?? pluginPaths();
|
|
11600
|
-
this.workDir =
|
|
11601
|
-
|
|
11602
|
-
this.
|
|
11603
|
-
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);
|
|
11604
12329
|
this.accounts = new Accounts(this);
|
|
11605
12330
|
this.operations = new Operations(this, this.accounts);
|
|
11606
12331
|
}
|
|
@@ -11638,7 +12363,8 @@ var Runtime = class _Runtime {
|
|
|
11638
12363
|
}
|
|
11639
12364
|
static async create(config2, options) {
|
|
11640
12365
|
const binary = resolveBinary(config2, options.projectDirectory);
|
|
11641
|
-
|
|
12366
|
+
removeStaleWorkDirs();
|
|
12367
|
+
const probeDir = mkdtempSync2(join9(tmpdir2(), "opencode-gws-probe-"));
|
|
11642
12368
|
let version2 = "unknown";
|
|
11643
12369
|
try {
|
|
11644
12370
|
const probe = await runProcess({
|
|
@@ -11655,7 +12381,7 @@ var Runtime = class _Runtime {
|
|
|
11655
12381
|
throw new Error(`GWS_VERSION_MISMATCH: Requires ${GWS_VERSION} and the matching manifest`);
|
|
11656
12382
|
}
|
|
11657
12383
|
} finally {
|
|
11658
|
-
|
|
12384
|
+
rmSync2(probeDir, { recursive: true, force: true });
|
|
11659
12385
|
}
|
|
11660
12386
|
return new _Runtime(config2, options, binary, version2);
|
|
11661
12387
|
}
|
|
@@ -11719,7 +12445,7 @@ var Runtime = class _Runtime {
|
|
|
11719
12445
|
this.plans.invalidateAll();
|
|
11720
12446
|
this.audit.close();
|
|
11721
12447
|
this.closing = settled.then(() => {
|
|
11722
|
-
|
|
12448
|
+
rmSync2(this.workDir, { recursive: true, force: true });
|
|
11723
12449
|
});
|
|
11724
12450
|
return this.closing;
|
|
11725
12451
|
}
|
|
@@ -11742,8 +12468,7 @@ async function start(ctx) {
|
|
|
11742
12468
|
const ids = integrationIds(config2.accounts);
|
|
11743
12469
|
const runtime = await Runtime.create(config2, {
|
|
11744
12470
|
pluginVersion: PLUGIN_VERSION,
|
|
11745
|
-
|
|
11746
|
-
hostWarning: host.warning,
|
|
12471
|
+
host,
|
|
11747
12472
|
projectDirectory: ctx.location.directory,
|
|
11748
12473
|
tokenProvider: integrationTokenProvider(ctx, config2, ids),
|
|
11749
12474
|
connection: integrationConnection(ctx, ids),
|