@joe-broadhead/opencode-gws 0.0.1 → 0.0.2
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 +8 -0
- package/README.md +4 -3
- package/dist/index.js +306 -201
- package/dist/tui.js +24 -1
- package/package.json +5 -5
- package/skills/opencode-gws/SKILL.md +2 -1
- package/skills/opencode-gws-admin/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.0.2] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The default sign-in no longer requests the Keep scopes, which Google grants only through domain-wide delegation, so Google no longer refuses the whole consent with `invalid_scope`. No access level requests `keep` or `keep.readonly`, and the `default` account no longer enables Keep: Read and write now requests 45 scopes and Read-only 37. `gws_catalog` and `gws_schema` mark the 7 Keep methods `restriction: "domain-wide-delegation"` (app-only methods keep `appOnly` and gain `restriction: "app-only"`), and the scope check refuses them with that reason. For an interactive account that enables `keep` without adding a Keep scope in `accounts.<alias>.scopes`, status carries `delegationRequired`, the sign-in form says Keep needs delegation, and `/gws-doctor` reports Keep `SKIPPED` with the reason. (#8)
|
|
10
|
+
- A non-admin's Admin SDK refusal of a valid sign-in, such as the Reports API's 401 `Access denied. You are not authorized to read activity records.`, is now `ADMIN_PRIVILEGE_REQUIRED`, not `AUTH_REQUIRED`. Its next step is to sign in with a Workspace admin account that has the privilege (Reports for `admin-reports`) or to remove the service from `enabledServices`. Reads, plans, commits and `/gws-doctor` classify it the same way; it never marks the account as needing sign-in in status or `/connect`, and the doctor reports it as its own status, counted as needing attention. Token failures (`invalid_grant`, an expired or revoked token, `UNAUTHENTICATED`) stay `AUTH_REQUIRED`. (#9)
|
|
11
|
+
- The tools RPC reports `SESSION_UNKNOWN` only when OpenCode says the session does not exist (`Session.NotFoundError`, or a 404). Any other failure to look the session up now propagates unchanged instead of being reported as an unknown session.
|
|
12
|
+
|
|
5
13
|
## [0.0.1] - 2026-11-03
|
|
6
14
|
|
|
7
15
|
### Added
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ opencode plugin add @joe-broadhead/opencode-gws
|
|
|
18
18
|
|
|
19
19
|
npm also installs the matching `@joe-broadhead/opencode-gws-bin-<target>` package, which holds the unmodified `gws` binary at the version the plugin pins ([details](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/backend.md)). Nothing downloads at runtime and there is no install script.
|
|
20
20
|
|
|
21
|
-
With no options, the plugin creates one account, `default`, with every Workspace service
|
|
21
|
+
With no options, the plugin creates one account, `default`, with every Workspace service a Google sign-in can use at Read and write, so one consent grants every scope it can use. Keep is left out: Google grants its scopes only through domain-wide delegation, which a Workspace admin sets up ([Keep](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/auth.md#keep-needs-domain-wide-delegation)). Every change still needs a plan and your approval. To narrow it, set `accounts` ([configuration](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/configuration.md#accounts)).
|
|
22
22
|
|
|
23
23
|
## Sign in
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ Run `/connect` and choose **Google Workspace**. The TUI shows the sign-in link:
|
|
|
28
28
|
opencode auth login gws-default
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
No Google OAuth client ships with the plugin. The first time, the form asks for the client ID and secret of your own Desktop app OAuth client, unless your organisation set one (see [Organisation rollout](#organisation-rollout)). To make one, follow [Create your Desktop client](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/auth.md#create-your-desktop-client). The form lists the exact scopes before Google's consent screen. Read and write is the default for the `default` account and needs an acknowledgment; Read-only is the other choice. The default consent lists
|
|
31
|
+
No Google OAuth client ships with the plugin. The first time, the form asks for the client ID and secret of your own Desktop app OAuth client, unless your organisation set one (see [Organisation rollout](#organisation-rollout)). To make one, follow [Create your Desktop client](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/auth.md#create-your-desktop-client). The form lists the exact scopes before Google's consent screen. Read and write is the default for the `default` account and needs an acknowledgment; Read-only is the other choice. The default consent lists 45 scopes (37 for Read-only), and an External client in Testing shows unverified-app warnings and expires refresh tokens after 7 days: see [What the default consent asks for](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/auth.md#what-the-default-consent-asks-for). With several accounts, each `/connect` entry is named **Google Workspace (`<alias>`)**.
|
|
32
32
|
|
|
33
33
|
Details and the scope table: [docs/auth.md](https://github.com/joe-broadhead/opencode-gws/blob/HEAD/docs/auth.md).
|
|
34
34
|
|
|
@@ -101,7 +101,8 @@ Members then only run `/connect`, choose Google Workspace, and pick an access le
|
|
|
101
101
|
| Code | What to do |
|
|
102
102
|
| --- | --- |
|
|
103
103
|
| `AUTH_REQUIRED` | Run `/connect` and choose Google Workspace, or run `opencode auth login gws-<alias>`. |
|
|
104
|
-
| `
|
|
104
|
+
| `ADMIN_PRIVILEGE_REQUIRED` | The sign-in is valid, but the account is not a Workspace admin with the privilege the API needs, such as Reports for `admin-reports`. Signing in again as the same account does not help: sign in with an admin account that has it, or remove the service from `enabledServices`. |
|
|
105
|
+
| `AUTH_SCOPE_MISSING` | Sign in again, approve every scope Google lists, and choose Read and write. A method marked `appOnly` (`restriction: "app-only"`) needs a Chat app's or Classroom add-on's own credentials and cannot run with a Google sign-in. A Keep method (`restriction: "domain-wide-delegation"`) needs domain-wide delegation that a Workspace admin sets up. Nothing ran. |
|
|
105
106
|
| `AUTH_CLIENT_REQUIRED` | Run `/connect` and choose Google Workspace, then enter your Desktop OAuth client ID and secret, or ask your administrator. |
|
|
106
107
|
| `API_DISABLED` | Enable the API at the URL in the message, wait about a minute, and retry. `/gws-doctor` checks every service. |
|
|
107
108
|
| `HOST_UNTESTED` | Use OpenCode 2.2.3 or a later 2.2.x, or set `"allowUntestedHost": true`. |
|
package/dist/index.js
CHANGED
|
@@ -6618,6 +6618,10 @@ var ErrorCode = {
|
|
|
6618
6618
|
meaning: "The sign-in has not granted a scope this operation needs. The plugin checks before gws runs, so nothing ran.",
|
|
6619
6619
|
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
6620
|
},
|
|
6621
|
+
ADMIN_PRIVILEGE_REQUIRED: {
|
|
6622
|
+
meaning: "The sign-in is valid, but Google refused the call because the account lacks a Workspace admin privilege the API needs, such as Reports for admin-reports. Signing in again as the same account does not help, and the sign-in is not marked as needing renewal.",
|
|
6623
|
+
next: "Ask the user to sign in with a Workspace admin account that has the needed privilege (Reports for admin-reports), or to remove the service from enabledServices."
|
|
6624
|
+
},
|
|
6621
6625
|
AUTH_CLIENT_REQUIRED: {
|
|
6622
6626
|
meaning: "No OAuth client is configured for the sign-in, none was entered in the form, and no earlier sign-in has one.",
|
|
6623
6627
|
next: "Run /connect and choose Google Workspace, then enter your Desktop OAuth client ID and secret, or ask your administrator."
|
|
@@ -6785,11 +6789,11 @@ import { readFileSync } from "node:fs";
|
|
|
6785
6789
|
import { dirname, join } from "node:path";
|
|
6786
6790
|
import { fileURLToPath } from "node:url";
|
|
6787
6791
|
|
|
6788
|
-
// src/domain/
|
|
6789
|
-
var
|
|
6790
|
-
|
|
6791
|
-
);
|
|
6792
|
-
var
|
|
6792
|
+
// src/domain/restricted.ts
|
|
6793
|
+
var PREFIX = "https://www.googleapis.com/auth/";
|
|
6794
|
+
var APP_ONLY_SCOPES = ["chat.bot", "chat.import", "classroom.addons.student", "classroom.addons.teacher"].map((name) => `${PREFIX}${name}`);
|
|
6795
|
+
var DELEGATION_SCOPES = ["keep", "keep.readonly"].map((name) => `${PREFIX}${name}`);
|
|
6796
|
+
var APP_ONLY_METHODS = [
|
|
6793
6797
|
// chat: a Chat app's own credentials
|
|
6794
6798
|
"chat.spaces.completeImport",
|
|
6795
6799
|
"chat.spaces.messages.attachments.get",
|
|
@@ -6822,9 +6826,33 @@ var USER_UNUSABLE_METHODS = [
|
|
|
6822
6826
|
"classroom.courses.posts.addOnAttachments.studentSubmissions.patch",
|
|
6823
6827
|
"classroom.courses.posts.getAddOnContext"
|
|
6824
6828
|
];
|
|
6825
|
-
var
|
|
6826
|
-
|
|
6827
|
-
|
|
6829
|
+
var DELEGATION_METHODS = [
|
|
6830
|
+
"keep.media.download",
|
|
6831
|
+
"keep.notes.create",
|
|
6832
|
+
"keep.notes.delete",
|
|
6833
|
+
"keep.notes.get",
|
|
6834
|
+
"keep.notes.list",
|
|
6835
|
+
"keep.notes.permissions.batchCreate",
|
|
6836
|
+
"keep.notes.permissions.batchDelete"
|
|
6837
|
+
];
|
|
6838
|
+
var DELEGATION_SERVICES = ["keep"];
|
|
6839
|
+
var RESTRICTIONS = new Map([
|
|
6840
|
+
...APP_ONLY_METHODS.map((id) => [id, "app-only"]),
|
|
6841
|
+
...DELEGATION_METHODS.map((id) => [id, "domain-wide-delegation"])
|
|
6842
|
+
]);
|
|
6843
|
+
function restriction(operation) {
|
|
6844
|
+
return RESTRICTIONS.get(operation);
|
|
6845
|
+
}
|
|
6846
|
+
function addsDelegationScope(scopes) {
|
|
6847
|
+
return scopes.some((scope) => DELEGATION_SCOPES.includes(scope));
|
|
6848
|
+
}
|
|
6849
|
+
function delegationRequired(services, scopes) {
|
|
6850
|
+
return addsDelegationScope(scopes) ? [] : services.filter((service) => DELEGATION_SERVICES.includes(service));
|
|
6851
|
+
}
|
|
6852
|
+
function restrictionMarks(operation) {
|
|
6853
|
+
const reason = restriction(operation);
|
|
6854
|
+
if (!reason) return {};
|
|
6855
|
+
return reason === "app-only" ? { appOnly: true, restriction: reason } : { restriction: reason };
|
|
6828
6856
|
}
|
|
6829
6857
|
|
|
6830
6858
|
// src/domain/helpers.ts
|
|
@@ -7350,7 +7378,7 @@ function catalog(input = {}, services) {
|
|
|
7350
7378
|
...method.pathTemplate ? { path: method.pathTemplate } : {},
|
|
7351
7379
|
executable: methodExecutable(method),
|
|
7352
7380
|
...tool && methodExecutable(method) ? { commitTool: tool } : {},
|
|
7353
|
-
...
|
|
7381
|
+
...restrictionMarks(method.id)
|
|
7354
7382
|
});
|
|
7355
7383
|
}
|
|
7356
7384
|
}
|
|
@@ -7513,7 +7541,8 @@ var DIRECT = {
|
|
|
7513
7541
|
forms: { read: ["forms.body.readonly", "forms.responses.readonly"], write: ["forms.body", "forms.responses.readonly", "drive.file"] },
|
|
7514
7542
|
// meetings.space.created reaches only spaces this client created; write keeps reading the others.
|
|
7515
7543
|
meet: { read: ["meetings.space.readonly"], write: ["meetings.space.created", "meetings.space.readonly"] },
|
|
7516
|
-
|
|
7544
|
+
// Google grants Keep's scopes only through domain-wide delegation (restricted.ts), so neither level requests them.
|
|
7545
|
+
keep: { read: [], write: [] },
|
|
7517
7546
|
// Reports are read-only at both levels.
|
|
7518
7547
|
"admin-reports": {
|
|
7519
7548
|
read: ["admin.reports.audit.readonly", "admin.reports.usage.readonly"],
|
|
@@ -7601,7 +7630,8 @@ var ClientJsonSchema = union([
|
|
|
7601
7630
|
var OAuthClientSchema = union([DesktopClientSchema, ClientJsonSchema]);
|
|
7602
7631
|
var accessLevel = _enum(["read", "write"]);
|
|
7603
7632
|
function defaultAccounts() {
|
|
7604
|
-
|
|
7633
|
+
const services = loadManifest().services.map((service) => service.alias).filter((service) => !DELEGATION_SERVICES.includes(service));
|
|
7634
|
+
return { default: { mode: "interactive", enabledServices: services, access: "write" } };
|
|
7605
7635
|
}
|
|
7606
7636
|
var enabledServices = array(string2().min(1)).min(1);
|
|
7607
7637
|
var accountProfile = preprocess(
|
|
@@ -7812,7 +7842,7 @@ function loadConfig(raw) {
|
|
|
7812
7842
|
}
|
|
7813
7843
|
|
|
7814
7844
|
// src/domain/version.ts
|
|
7815
|
-
var PLUGIN_VERSION = "0.0.
|
|
7845
|
+
var PLUGIN_VERSION = "0.0.2";
|
|
7816
7846
|
var PLUGIN_ID = "google-workspace.gws";
|
|
7817
7847
|
var HOST_RANGE = ">=2.2.3 <2.3.0";
|
|
7818
7848
|
var SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -7932,6 +7962,7 @@ var account = object2(
|
|
|
7932
7962
|
clientSource: { enum: CLIENT_SOURCES },
|
|
7933
7963
|
grantedScopes: texts,
|
|
7934
7964
|
missingScopes: texts,
|
|
7965
|
+
delegationRequired: texts,
|
|
7935
7966
|
nextStep: text
|
|
7936
7967
|
},
|
|
7937
7968
|
["alias", "mode", "services", "connected"]
|
|
@@ -7969,6 +8000,11 @@ function connectionLabel(account2) {
|
|
|
7969
8000
|
function openCodeLabel(status2) {
|
|
7970
8001
|
return status2.hostWarning ? `${status2.hostVersion} (host untested: ${status2.hostWarning})` : `${status2.hostVersion} (tested)`;
|
|
7971
8002
|
}
|
|
8003
|
+
var DELEGATION_LABEL = "Needs domain-wide delegation";
|
|
8004
|
+
function delegationText(account2) {
|
|
8005
|
+
const services = account2.delegationRequired ?? [];
|
|
8006
|
+
return `${services.join(", ")} (a Workspace admin sets it up; then add the scopes to accounts.${account2.alias}.scopes)`;
|
|
8007
|
+
}
|
|
7972
8008
|
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
8009
|
|
|
7974
8010
|
// src/auth/setup.ts
|
|
@@ -8163,6 +8199,199 @@ var PlanStore = class {
|
|
|
8163
8199
|
}
|
|
8164
8200
|
};
|
|
8165
8201
|
|
|
8202
|
+
// src/domain/result.ts
|
|
8203
|
+
function isApiError(value) {
|
|
8204
|
+
if (Array.isArray(value)) return value.some(isApiError);
|
|
8205
|
+
if (!value || typeof value !== "object") return false;
|
|
8206
|
+
const record2 = value;
|
|
8207
|
+
return "error" in record2 && record2.error !== null && typeof record2.error === "object";
|
|
8208
|
+
}
|
|
8209
|
+
function parseJson(text3) {
|
|
8210
|
+
const lines = text3.split("\n");
|
|
8211
|
+
const start2 = lines.findIndex((line) => /^[[{]/.test(line.trim()));
|
|
8212
|
+
if (start2 < 0) return void 0;
|
|
8213
|
+
const candidate = lines.slice(start2).join("\n").trim();
|
|
8214
|
+
try {
|
|
8215
|
+
return JSON.parse(candidate);
|
|
8216
|
+
} catch {
|
|
8217
|
+
const parsed = [];
|
|
8218
|
+
for (const line of lines.slice(start2)) {
|
|
8219
|
+
const value = line.trim();
|
|
8220
|
+
if (!/^[[{]/.test(value)) continue;
|
|
8221
|
+
try {
|
|
8222
|
+
parsed.push(JSON.parse(value));
|
|
8223
|
+
} catch {
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
8227
|
+
}
|
|
8228
|
+
}
|
|
8229
|
+
function inlineCap(maxInlineBytes, mode, requested) {
|
|
8230
|
+
const base2 = mode === "full" ? Math.max(maxInlineBytes, 5 * 1024 * 1024) : maxInlineBytes;
|
|
8231
|
+
return requested === void 0 ? base2 : Math.min(base2, requested);
|
|
8232
|
+
}
|
|
8233
|
+
function parseResult(result, options) {
|
|
8234
|
+
const text3 = result.stdout.toString("utf8");
|
|
8235
|
+
const parsed = parseJson(text3);
|
|
8236
|
+
const serialized = parsed === void 0 ? text3 : JSON.stringify(parsed);
|
|
8237
|
+
const bytes2 = Buffer.byteLength(serialized);
|
|
8238
|
+
const cap = inlineCap(options.maxInlineBytes, options.mode, options.requestedInlineBytes);
|
|
8239
|
+
const truncated = bytes2 > cap;
|
|
8240
|
+
const empty = text3.trim().length === 0;
|
|
8241
|
+
return {
|
|
8242
|
+
// A successful DELETE commonly returns 204 with no body. Empty output
|
|
8243
|
+
// with exit 0 is success; non-JSON text is not a JSON result.
|
|
8244
|
+
ok: result.exitCode === 0 && (empty || parsed !== void 0 && !isApiError(parsed)),
|
|
8245
|
+
data: parsed,
|
|
8246
|
+
raw: parsed === void 0 ? text3.slice(0, cap) : void 0,
|
|
8247
|
+
truncated,
|
|
8248
|
+
diagnostics: { exitCode: result.exitCode, durationMs: result.durationMs, bytes: bytes2 }
|
|
8249
|
+
};
|
|
8250
|
+
}
|
|
8251
|
+
function itemKey(page2) {
|
|
8252
|
+
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return void 0;
|
|
8253
|
+
const record2 = page2;
|
|
8254
|
+
if (Array.isArray(record2["items"])) return "items";
|
|
8255
|
+
const arrays = Object.keys(record2).filter((key) => Array.isArray(record2[key]));
|
|
8256
|
+
return arrays.length === 1 ? arrays[0] : void 0;
|
|
8257
|
+
}
|
|
8258
|
+
function capPageItems(data, maxItems) {
|
|
8259
|
+
const pages = data === void 0 ? [] : Array.isArray(data) ? data : [data];
|
|
8260
|
+
const keys = pages.map((page2) => itemKey(page2) ?? (isEmptyPage(page2) ? null : void 0));
|
|
8261
|
+
if (keys.some((key) => key === void 0)) return { data: pages, pages: pages.length, capped: false };
|
|
8262
|
+
const kept = [];
|
|
8263
|
+
let items = 0;
|
|
8264
|
+
let capped = false;
|
|
8265
|
+
for (const [index2, page2] of pages.entries()) {
|
|
8266
|
+
if (items >= maxItems) {
|
|
8267
|
+
capped = true;
|
|
8268
|
+
break;
|
|
8269
|
+
}
|
|
8270
|
+
const key = keys[index2];
|
|
8271
|
+
if (!key) {
|
|
8272
|
+
kept.push(page2);
|
|
8273
|
+
continue;
|
|
8274
|
+
}
|
|
8275
|
+
const record2 = page2;
|
|
8276
|
+
const list = record2[key];
|
|
8277
|
+
const room = maxItems - items;
|
|
8278
|
+
if (list.length > room) {
|
|
8279
|
+
const { nextPageToken: _dropped, ...rest } = record2;
|
|
8280
|
+
kept.push({ ...rest, [key]: list.slice(0, room) });
|
|
8281
|
+
items += room;
|
|
8282
|
+
capped = true;
|
|
8283
|
+
break;
|
|
8284
|
+
}
|
|
8285
|
+
kept.push(page2);
|
|
8286
|
+
items += list.length;
|
|
8287
|
+
}
|
|
8288
|
+
return { data: kept, pages: kept.length, items, capped };
|
|
8289
|
+
}
|
|
8290
|
+
function isEmptyPage(page2) {
|
|
8291
|
+
if (!page2 || typeof page2 !== "object" || Array.isArray(page2)) return false;
|
|
8292
|
+
return !Object.values(page2).some(Array.isArray);
|
|
8293
|
+
}
|
|
8294
|
+
function redactDryRun(value) {
|
|
8295
|
+
if (Array.isArray(value)) return value.map(redactDryRun);
|
|
8296
|
+
if (value && typeof value === "object") {
|
|
8297
|
+
const out = {};
|
|
8298
|
+
for (const [key, child] of Object.entries(value)) {
|
|
8299
|
+
if (/^(body|raw|content)$/i.test(key)) out[key] = "[redacted]";
|
|
8300
|
+
else out[key] = redactDryRun(child);
|
|
8301
|
+
}
|
|
8302
|
+
return out;
|
|
8303
|
+
}
|
|
8304
|
+
return value;
|
|
8305
|
+
}
|
|
8306
|
+
function apiErrorOf(result, parsed) {
|
|
8307
|
+
const data = parsed ? parsed.data : parseJson(result.stdout.toString("utf8"));
|
|
8308
|
+
const found = Array.isArray(data) ? data.find(isApiError) : data;
|
|
8309
|
+
if (!isApiError(found)) return void 0;
|
|
8310
|
+
return found.error;
|
|
8311
|
+
}
|
|
8312
|
+
function stderrText(result) {
|
|
8313
|
+
return result.stderr.toString("utf8").split("\n").filter((line) => line.trim() && !/^Using keyring backend:/i.test(line)).join("\n").trim();
|
|
8314
|
+
}
|
|
8315
|
+
var EXIT = { auth: 2, validation: 3, discovery: 4, internal: 5 };
|
|
8316
|
+
var SCOPE_REASONS = /* @__PURE__ */ new Set(["ACCESS_TOKEN_SCOPE_INSUFFICIENT", "insufficientPermissions"]);
|
|
8317
|
+
var PRIVILEGE_REFUSALS = /\bnot authori[sz]ed to (?:read|access|view|list|use|manage|perform)\b|\bcaller does not have access to the customers? reporting data\b/i;
|
|
8318
|
+
function privilegeRefused(result, parsed) {
|
|
8319
|
+
if (result.timedOut || result.outputCapped || result.exitCode === 0 || result.exitCode === EXIT.auth) return false;
|
|
8320
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8321
|
+
if (error2?.code !== void 0 && error2.code !== 401 && error2.code !== 403) return false;
|
|
8322
|
+
const text3 = [error2?.message, stderrText(result)].filter(Boolean).join("\n");
|
|
8323
|
+
return !/invalid_grant/i.test(text3) && PRIVILEGE_REFUSALS.test(text3);
|
|
8324
|
+
}
|
|
8325
|
+
var ADMIN_PRIVILEGES = { "admin-reports": "Reports" };
|
|
8326
|
+
function privilegeName(service) {
|
|
8327
|
+
const name = service ? ADMIN_PRIVILEGES[service] : void 0;
|
|
8328
|
+
return name ? `the ${name} privilege` : "the admin privilege it needs";
|
|
8329
|
+
}
|
|
8330
|
+
function authProblem(result, parsed) {
|
|
8331
|
+
if (result.timedOut || result.outputCapped) return void 0;
|
|
8332
|
+
if (privilegeRefused(result, parsed)) return void 0;
|
|
8333
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8334
|
+
if (result.exitCode === 0 && !error2) return void 0;
|
|
8335
|
+
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
8336
|
+
const text3 = [error2?.message, error2?.status, stderrText(result)].filter(Boolean).join("\n");
|
|
8337
|
+
if (reasons.some((reason) => reason !== void 0 && SCOPE_REASONS.has(reason)) || /insufficient authentication scopes|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(text3)) {
|
|
8338
|
+
return "AUTH_SCOPE_MISSING";
|
|
8339
|
+
}
|
|
8340
|
+
if (error2?.code === 401 || error2?.status === "UNAUTHENTICATED" || reasons.includes("authError") || /invalid_grant|\bUNAUTHENTICATED\b|^error\[auth\]/im.test(text3)) {
|
|
8341
|
+
return "AUTH_REQUIRED";
|
|
8342
|
+
}
|
|
8343
|
+
return void 0;
|
|
8344
|
+
}
|
|
8345
|
+
var GENERIC_REMEDIATION = {
|
|
8346
|
+
signIn: "sign in to the account again",
|
|
8347
|
+
consent: "sign in again and grant the scope this operation needs",
|
|
8348
|
+
privilege: "sign in with a Workspace admin account that has the admin privilege this operation needs, or remove the service from enabledServices"
|
|
8349
|
+
};
|
|
8350
|
+
var DISABLED_REASONS = /* @__PURE__ */ new Set(["accessNotConfigured", "SERVICE_DISABLED"]);
|
|
8351
|
+
var CONSOLE_URL = /https:\/\/console\.(?:developers|cloud)\.google\.com\/[^\s"'<>)]+/;
|
|
8352
|
+
function disabledApi(result, parsed) {
|
|
8353
|
+
const error2 = apiErrorOf(result, parsed);
|
|
8354
|
+
const reasons = [error2?.reason, ...(error2?.errors ?? []).map((item) => item?.reason), ...(error2?.details ?? []).map((item) => item?.reason)];
|
|
8355
|
+
const text3 = [error2?.message, stderrText(result)].filter(Boolean).join("\n");
|
|
8356
|
+
if (!reasons.some((reason) => reason !== void 0 && DISABLED_REASONS.has(reason)) && !/accessnotconfigured|has not been used|is disabled/i.test(text3)) {
|
|
8357
|
+
return void 0;
|
|
8358
|
+
}
|
|
8359
|
+
const enableUrl = (typeof error2?.enable_url === "string" && CONSOLE_URL.test(error2.enable_url) ? error2.enable_url : CONSOLE_URL.exec(text3)?.[0])?.replace(/[.,;]+$/, "");
|
|
8360
|
+
const project = (enableUrl ? /[?&]project=([\w.:-]+)/.exec(enableUrl)?.[1] : void 0) ?? /\bin project ([\w.:-]+)/.exec(text3)?.[1];
|
|
8361
|
+
return { ...enableUrl ? { enableUrl } : {}, ...project ? { project } : {} };
|
|
8362
|
+
}
|
|
8363
|
+
function processError(result, parsed, remediation = GENERIC_REMEDIATION) {
|
|
8364
|
+
if (result.timedOut) return new GwsError("TIMEOUT", `gws did not finish within the configured execution.timeoutMs (${result.durationMs}ms elapsed)`);
|
|
8365
|
+
if (result.outputCapped) return new GwsError("OUTPUT_LIMIT", "gws output exceeded execution.maxStdoutBytes/maxStderrBytes; narrow fields or pagination");
|
|
8366
|
+
const stderr = stderrText(result);
|
|
8367
|
+
const payload = apiErrorOf(result, parsed);
|
|
8368
|
+
const message = payload?.message || stderr || "gws command failed";
|
|
8369
|
+
if (privilegeRefused(result, parsed)) {
|
|
8370
|
+
return new GwsError(
|
|
8371
|
+
"ADMIN_PRIVILEGE_REQUIRED",
|
|
8372
|
+
`Google accepted the sign-in but refused the call, because the account lacks a Workspace admin privilege; signing in again as the same account cannot help (gws: ${message.slice(0, 300)})`,
|
|
8373
|
+
{ next: `${remediation.privilege.charAt(0).toUpperCase()}${remediation.privilege.slice(1)}.` }
|
|
8374
|
+
);
|
|
8375
|
+
}
|
|
8376
|
+
const problem = authProblem(result, parsed) ?? (result.exitCode === EXIT.auth ? "AUTH_REQUIRED" : void 0);
|
|
8377
|
+
if (problem === "AUTH_SCOPE_MISSING") {
|
|
8378
|
+
return new GwsError("AUTH_SCOPE_MISSING", `the credential lacks a scope for this operation; ${remediation.consent} (gws: ${message.slice(0, 300)})`);
|
|
8379
|
+
}
|
|
8380
|
+
if (problem === "AUTH_REQUIRED") {
|
|
8381
|
+
return new GwsError("AUTH_REQUIRED", `the Google Workspace sign-in is missing or no longer valid; ${remediation.signIn} (gws: ${message.slice(0, 300)})`);
|
|
8382
|
+
}
|
|
8383
|
+
const disabled = disabledApi(result, parsed);
|
|
8384
|
+
if (disabled) {
|
|
8385
|
+
const where = disabled.project ? ` for project ${disabled.project}` : "";
|
|
8386
|
+
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.`;
|
|
8387
|
+
return new GwsError("API_DISABLED", message.slice(0, 500), { next, ...disabled.enableUrl ? { enableUrl: disabled.enableUrl } : {} });
|
|
8388
|
+
}
|
|
8389
|
+
if (result.exitCode === EXIT.validation) return new GwsError("GWS_VALIDATION_ERROR", `gws rejected the request: ${message.slice(0, 500)}`);
|
|
8390
|
+
if (result.exitCode === EXIT.discovery) return new GwsError("GWS_DISCOVERY_ERROR", `gws could not load the API description: ${message.slice(0, 500)}`);
|
|
8391
|
+
if (result.exitCode === EXIT.internal) return new GwsError("GWS_INTERNAL_ERROR", `gws failed: ${message.slice(0, 500)}`);
|
|
8392
|
+
return new GwsError("GWS_API_ERROR", message.slice(0, 500));
|
|
8393
|
+
}
|
|
8394
|
+
|
|
8166
8395
|
// src/host/commands.ts
|
|
8167
8396
|
var COMMANDS = {
|
|
8168
8397
|
status: { name: "gws-status", description: "Google Workspace: sign-in, access, pending plans, setup steps, and unguarded agents" },
|
|
@@ -8219,6 +8448,7 @@ function accountLines(account2) {
|
|
|
8219
8448
|
lines.push(`Services: ${account2.services.join(", ") || "none"}`);
|
|
8220
8449
|
if (account2.grantedScopes) lines.push(`Granted scopes: ${scopeList(account2.grantedScopes)}`);
|
|
8221
8450
|
if (account2.missingScopes?.length) lines.push(`Scopes not granted: ${scopeList(account2.missingScopes)}`);
|
|
8451
|
+
if (account2.delegationRequired?.length) lines.push(`${DELEGATION_LABEL}: ${delegationText(account2)}`);
|
|
8222
8452
|
return lines;
|
|
8223
8453
|
}
|
|
8224
8454
|
function formatStatus(status2, plans = [], now = Date.now()) {
|
|
@@ -8289,6 +8519,8 @@ function nextFor(outcome) {
|
|
|
8289
8519
|
return `enable it at ${outcome.enableUrl}, wait about a minute, and run /gws-doctor again`;
|
|
8290
8520
|
case "DENIED":
|
|
8291
8521
|
return "allow gws.read for this account in your permissions, or skip the check";
|
|
8522
|
+
case "ADMIN_PRIVILEGE_REQUIRED":
|
|
8523
|
+
return `sign in with a Workspace admin account that has ${privilegeName(outcome.service)}, or remove ${outcome.service} from accounts.${outcome.account}.enabledServices`;
|
|
8292
8524
|
default:
|
|
8293
8525
|
return void 0;
|
|
8294
8526
|
}
|
|
@@ -8424,6 +8656,11 @@ function accessOptions(profile) {
|
|
|
8424
8656
|
if (!offersWrite(profile)) return [read];
|
|
8425
8657
|
return [read, { value: "write", label: "Read and write", description: `Requests: ${signInScopes(profile, "write").join(" ")}` }];
|
|
8426
8658
|
}
|
|
8659
|
+
function delegationNote(alias, profile) {
|
|
8660
|
+
const services = delegationRequired(profile.enabledServices, profile.scopes);
|
|
8661
|
+
if (services.length === 0) return "";
|
|
8662
|
+
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.`;
|
|
8663
|
+
}
|
|
8427
8664
|
function defaultAccess(profile) {
|
|
8428
8665
|
const access = profile.access;
|
|
8429
8666
|
return access === "write" || typeof access === "object" && Object.values(access).includes("write") ? "write" : "read";
|
|
@@ -8500,7 +8737,7 @@ function signInForm(alias, profile, configured, redirect = {}) {
|
|
|
8500
8737
|
key: "access",
|
|
8501
8738
|
type: "string",
|
|
8502
8739
|
title: "Access",
|
|
8503
|
-
description: `What ${alias} may do in Google Workspace. Read-only cannot change, send, share, or delete anything
|
|
8740
|
+
description: `What ${alias} may do in Google Workspace. Read-only cannot change, send, share, or delete anything.${delegationNote(alias, profile)}`,
|
|
8504
8741
|
required: true,
|
|
8505
8742
|
default: defaultAccess(profile),
|
|
8506
8743
|
options: accessOptions(profile)
|
|
@@ -9304,6 +9541,9 @@ var Diagnostics = object({
|
|
|
9304
9541
|
bytes: number2().describe("Serialized size of data")
|
|
9305
9542
|
}).describe("gws process facts");
|
|
9306
9543
|
var FileInfo = object({ path: string2(), bytes: number2(), sha256: string2() }).describe("A local file the plugin delivered");
|
|
9544
|
+
var Restriction = _enum(["app-only", "domain-wide-delegation"]).describe(
|
|
9545
|
+
"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"
|
|
9546
|
+
);
|
|
9307
9547
|
var CatalogEntry = object({
|
|
9308
9548
|
kind: _enum(["service", "method", "helper"]),
|
|
9309
9549
|
id: string2().describe("Pass a method or helper id as operation"),
|
|
@@ -9315,7 +9555,8 @@ var CatalogEntry = object({
|
|
|
9315
9555
|
executable: boolean2().describe("The plugin can run it (a service: at least one of its operations); false for denied, deploy, and watch entries"),
|
|
9316
9556
|
commitTool: string2().optional().describe('The tool that commits its plan, e.g. "gws_send"; absent for reads'),
|
|
9317
9557
|
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")
|
|
9558
|
+
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"),
|
|
9559
|
+
restriction: Restriction.optional()
|
|
9319
9560
|
});
|
|
9320
9561
|
var CatalogOutput = object({
|
|
9321
9562
|
entries: array(CatalogEntry),
|
|
@@ -9343,6 +9584,7 @@ var SchemaOutput = object({
|
|
|
9343
9584
|
account: string2(),
|
|
9344
9585
|
schema: ResolvedSchema.optional().describe("Present for a method"),
|
|
9345
9586
|
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"),
|
|
9587
|
+
restriction: Restriction.optional(),
|
|
9346
9588
|
helper: HelperEntry.extend({
|
|
9347
9589
|
input: unknown().optional().describe("JSON Schema of the helper's input flags; absent when the helper cannot run")
|
|
9348
9590
|
}).optional().describe("Present for a helper")
|
|
@@ -9392,177 +9634,6 @@ var CommitOutput = object({
|
|
|
9392
9634
|
cleanupDegraded: boolean2().optional().describe("The change succeeded, but private staging cleanup failed; do not retry the mutation")
|
|
9393
9635
|
});
|
|
9394
9636
|
|
|
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
9637
|
// src/domain/status.ts
|
|
9567
9638
|
var AccountStatus = object({
|
|
9568
9639
|
alias: string2(),
|
|
@@ -9575,6 +9646,7 @@ var AccountStatus = object({
|
|
|
9575
9646
|
clientSource: _enum(["account", "plugin", "pasted"]).optional().describe("Where the sign-in's OAuth client comes from"),
|
|
9576
9647
|
grantedScopes: array(string2()).optional().describe("Scopes Google granted to the current sign-in"),
|
|
9577
9648
|
missingScopes: array(string2()).optional().describe("Scopes the enabled services need at this access level that were not granted"),
|
|
9649
|
+
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
9650
|
nextStep: string2().optional().describe("What to do next; absent when nothing is needed")
|
|
9579
9651
|
});
|
|
9580
9652
|
var StatusOutput = object({
|
|
@@ -9821,6 +9893,12 @@ var Description = strictObject({
|
|
|
9821
9893
|
})
|
|
9822
9894
|
)
|
|
9823
9895
|
});
|
|
9896
|
+
function sessionNotFound(error2) {
|
|
9897
|
+
if (error2 === null || typeof error2 !== "object") return false;
|
|
9898
|
+
const record2 = error2;
|
|
9899
|
+
const tag = record2["_tag"] ?? record2["name"];
|
|
9900
|
+
return tag === "Session.NotFoundError" || tag === "SessionNotFoundError" || record2["status"] === 404 || record2["statusCode"] === 404;
|
|
9901
|
+
}
|
|
9824
9902
|
async function registerScripting(ctx, runtime, status2) {
|
|
9825
9903
|
const assert2 = async (request) => {
|
|
9826
9904
|
try {
|
|
@@ -9836,8 +9914,8 @@ async function registerScripting(ctx, runtime, status2) {
|
|
|
9836
9914
|
try {
|
|
9837
9915
|
agent = (await runtime.wait(() => ctx.session.get({ sessionID }), signal)).agent;
|
|
9838
9916
|
} catch (error2) {
|
|
9839
|
-
if (
|
|
9840
|
-
throw
|
|
9917
|
+
if (sessionNotFound(error2)) throw new GwsError("SESSION_UNKNOWN", `there is no session '${sessionID}'`);
|
|
9918
|
+
throw error2;
|
|
9841
9919
|
}
|
|
9842
9920
|
if (!agent) throw new GwsError("SESSION_AGENT_REQUIRED", `session '${sessionID}' names no agent`);
|
|
9843
9921
|
return { sessionID, agent, script: true };
|
|
@@ -10438,10 +10516,17 @@ var PROBES = {
|
|
|
10438
10516
|
"admin-reports": { operation: "admin-reports.activities.list", params: { userKey: "all", applicationName: "login", maxResults: 1, fields: "items(id)" } },
|
|
10439
10517
|
script: { operation: "script.processes.list", params: { pageSize: 1, fields: "processes(processType)" } }
|
|
10440
10518
|
};
|
|
10441
|
-
function doctorTarget(account2, service) {
|
|
10519
|
+
function doctorTarget(account2, service, needsDelegation = false) {
|
|
10520
|
+
if (needsDelegation) return { account: account2, service, needsDelegation: true };
|
|
10442
10521
|
const probe = PROBES[service];
|
|
10443
10522
|
return probe ? { account: account2, service, operation: probe.operation } : { account: account2, service };
|
|
10444
10523
|
}
|
|
10524
|
+
function skipDetail(target) {
|
|
10525
|
+
if (target.needsDelegation) {
|
|
10526
|
+
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`;
|
|
10527
|
+
}
|
|
10528
|
+
return "no read-only check exists for this service";
|
|
10529
|
+
}
|
|
10445
10530
|
function probeArgv(target) {
|
|
10446
10531
|
const probe = PROBES[target.service];
|
|
10447
10532
|
const entry = probe ? commandIndex().methods.get(probe.operation) : void 0;
|
|
@@ -10452,6 +10537,9 @@ var DISABLED = /accessNotConfigured|SERVICE_DISABLED|has not been used in projec
|
|
|
10452
10537
|
function probeOutcome(target, result) {
|
|
10453
10538
|
const parsed = parseResult(result, { maxInlineBytes: 64 * 1024 });
|
|
10454
10539
|
if (parsed.ok) return { ...target, status: "OK" };
|
|
10540
|
+
if (privilegeRefused(result, parsed)) {
|
|
10541
|
+
return { ...target, status: "ADMIN_PRIVILEGE_REQUIRED", detail: "Google accepted the sign-in, but the account lacks the Workspace admin privilege this service needs" };
|
|
10542
|
+
}
|
|
10455
10543
|
const problem = authProblem(result, parsed);
|
|
10456
10544
|
if (problem === "AUTH_REQUIRED") return { ...target, status: problem, detail: "Google rejected the sign-in" };
|
|
10457
10545
|
if (problem === "AUTH_SCOPE_MISSING") return { ...target, status: problem, detail: "Google says the sign-in lacks a scope for this service" };
|
|
@@ -10546,6 +10634,7 @@ var Accounts = class {
|
|
|
10546
10634
|
mode: profile.mode,
|
|
10547
10635
|
services: profile.enabledServices,
|
|
10548
10636
|
...profile.mode === "interactive" ? { integrationID } : {},
|
|
10637
|
+
...delegationField(profile),
|
|
10549
10638
|
...signIn
|
|
10550
10639
|
};
|
|
10551
10640
|
})
|
|
@@ -10612,9 +10701,14 @@ var Accounts = class {
|
|
|
10612
10701
|
return credential;
|
|
10613
10702
|
}
|
|
10614
10703
|
scopeMissing(account2, profile, operation, accepted) {
|
|
10615
|
-
|
|
10704
|
+
const reason = restriction(operation);
|
|
10705
|
+
if (reason === "app-only") {
|
|
10616
10706
|
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
10707
|
}
|
|
10708
|
+
if (reason === "domain-wide-delegation") {
|
|
10709
|
+
const named2 = accepted.map(shortScope).join(" or ");
|
|
10710
|
+
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.`;
|
|
10711
|
+
}
|
|
10618
10712
|
const read = signInScopes(profile, "read");
|
|
10619
10713
|
const named = accepted.slice(0, 5).map(shortScope).join(", ");
|
|
10620
10714
|
const connect2 = this.connect(account2);
|
|
@@ -10631,24 +10725,27 @@ var Accounts = class {
|
|
|
10631
10725
|
accountDir(account2) {
|
|
10632
10726
|
return ownedDir(join6(this.runtime.paths.cacheDir, "accounts", this.integrationId(account2).replace(/^gws-/, "")));
|
|
10633
10727
|
}
|
|
10634
|
-
/** Remediation text for an account's sign-in and
|
|
10635
|
-
remediation(account2, scopes) {
|
|
10728
|
+
/** Remediation text for an account's sign-in, consent and admin privilege problems, by credential mode. */
|
|
10729
|
+
remediation(account2, scopes, service) {
|
|
10636
10730
|
const profile = this.runtime.config.accounts[account2];
|
|
10637
10731
|
const named = scopes && scopes.length > 0 ? scopes.slice(0, 5).map(shortScope).join(", ") : void 0;
|
|
10638
10732
|
const grant = named ? `include one of: ${named}` : "include the scope this operation needs";
|
|
10733
|
+
const admin = `a Workspace admin account that has ${privilegeName(service)}`;
|
|
10734
|
+
const remove = `remove ${service ?? "the service"} from accounts.${account2}.enabledServices`;
|
|
10639
10735
|
if (profile?.mode === "credentials_file") {
|
|
10640
10736
|
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}` };
|
|
10737
|
+
return { signIn: `replace ${source}`, consent: `replace ${source} with one whose grant can ${grant}`, privilege: `replace ${source} with one for ${admin}, or ${remove}` };
|
|
10642
10738
|
}
|
|
10643
10739
|
if (profile?.mode === "access_token") {
|
|
10644
10740
|
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}` };
|
|
10741
|
+
return { signIn: `refresh ${source}`, consent: `mint ${source} with scopes that ${grant}`, privilege: `mint ${source} for ${admin}, or ${remove}` };
|
|
10646
10742
|
}
|
|
10647
10743
|
const connect2 = this.connect(account2);
|
|
10648
10744
|
const extra = named ? `, or add one of ${named} to accounts.${account2}.scopes` : "";
|
|
10649
10745
|
return {
|
|
10650
10746
|
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}
|
|
10747
|
+
consent: `to grant the scope, ${connect2}, sign in again, and approve every scope, choosing Read and write for a change${extra}`,
|
|
10748
|
+
privilege: `sign in with ${admin} (${connect2}), or ${remove}`
|
|
10652
10749
|
};
|
|
10653
10750
|
}
|
|
10654
10751
|
authMessage(account2, code2, scopes) {
|
|
@@ -10667,11 +10764,13 @@ var Accounts = class {
|
|
|
10667
10764
|
/** Every account and enabled service /gws-doctor checks, in config order. */
|
|
10668
10765
|
doctorTargets() {
|
|
10669
10766
|
this.runtime.assertActive();
|
|
10670
|
-
return Object.entries(this.runtime.config.accounts).flatMap(
|
|
10767
|
+
return Object.entries(this.runtime.config.accounts).flatMap(
|
|
10768
|
+
([alias, profile]) => profile.enabledServices.map((service) => doctorTarget(alias, service, profile.mode === "interactive" && delegationRequired([service], profile.scopes).length > 0))
|
|
10769
|
+
);
|
|
10671
10770
|
}
|
|
10672
10771
|
/** Run one /gws-doctor probe as the account. A failure becomes an outcome, never a throw, except cancellation. */
|
|
10673
10772
|
async probe(target, ctx) {
|
|
10674
|
-
if (!target.operation) return { ...target, status: "SKIPPED", detail:
|
|
10773
|
+
if (!target.operation) return { ...target, status: "SKIPPED", detail: skipDetail(target) };
|
|
10675
10774
|
const started = Date.now();
|
|
10676
10775
|
try {
|
|
10677
10776
|
const result = await this.run(probeArgv(target), target.account, target.operation, ctx);
|
|
@@ -10712,6 +10811,11 @@ function report2(ctx, update) {
|
|
|
10712
10811
|
} catch {
|
|
10713
10812
|
}
|
|
10714
10813
|
}
|
|
10814
|
+
function delegationField(profile) {
|
|
10815
|
+
if (profile.mode !== "interactive") return {};
|
|
10816
|
+
const services = delegationRequired(profile.enabledServices, profile.scopes);
|
|
10817
|
+
return services.length > 0 ? { delegationRequired: services } : {};
|
|
10818
|
+
}
|
|
10715
10819
|
function capitalize(text3) {
|
|
10716
10820
|
return text3.charAt(0).toUpperCase() + text3.slice(1);
|
|
10717
10821
|
}
|
|
@@ -11371,7 +11475,7 @@ var Operations = class {
|
|
|
11371
11475
|
report2(ctx, { status: "running", operation: entry.id });
|
|
11372
11476
|
const schema = await this.runtime.schema.resolve(entry.service, [...entry.path, entry.method], ctx.signal);
|
|
11373
11477
|
this.runtime.assertActive(ctx.signal);
|
|
11374
|
-
return { ok: true, account: target.account, schema, ...
|
|
11478
|
+
return { ok: true, account: target.account, schema, ...restrictionMarks(entry.id) };
|
|
11375
11479
|
}
|
|
11376
11480
|
/** Validate a commit against its stored plan without consuming it. */
|
|
11377
11481
|
inspectPlan(category, input, ctx) {
|
|
@@ -11396,7 +11500,7 @@ var Operations = class {
|
|
|
11396
11500
|
requestedInlineBytes: helper ? void 0 : request.response?.maxInlineBytes
|
|
11397
11501
|
});
|
|
11398
11502
|
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));
|
|
11503
|
+
if (failed) throw processError(result, parsed, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11400
11504
|
const pages = paged ? capPageItems(parsed.data, itemCap(request, limits)) : void 0;
|
|
11401
11505
|
const cap = inlineCap(this.runtime.config.execution.maxInlineBytes, request.response?.mode, request.response?.maxInlineBytes);
|
|
11402
11506
|
const truncated = pages ? Buffer.byteLength(JSON.stringify(pages.data)) > cap : parsed.truncated;
|
|
@@ -11460,7 +11564,7 @@ var Operations = class {
|
|
|
11460
11564
|
const dry = await this.accounts.run(dryRunArgv, request.account, entry.id, ctx, { scopes: entry.scopes, dryRun: true, cwd: stage?.dir });
|
|
11461
11565
|
report2(ctx, { status: "parsing", operation: entry.id });
|
|
11462
11566
|
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));
|
|
11567
|
+
if (dry.exitCode !== 0 || !parsed.ok) throw processError(dry, parsed, this.accounts.remediation(request.account, entry.scopes, entry.service));
|
|
11464
11568
|
} finally {
|
|
11465
11569
|
await stage?.dispose();
|
|
11466
11570
|
}
|
|
@@ -11518,11 +11622,12 @@ var Operations = class {
|
|
|
11518
11622
|
refuseChanged(plan.files.inputs, staged);
|
|
11519
11623
|
if (plan.files.output) await stage.prepareOutput(plan.files.output);
|
|
11520
11624
|
}
|
|
11521
|
-
const
|
|
11625
|
+
const method = commandIndex().methods.get(plan.operation);
|
|
11626
|
+
const scopes = method?.scopes;
|
|
11522
11627
|
const result = await this.accounts.run(plan.argv, plan.account, plan.operation, ctx, { scopes, cwd: stage?.dir });
|
|
11523
11628
|
report2(ctx, { status: "parsing", operation: plan.operation });
|
|
11524
11629
|
const parsed = parseResult(result, { maxInlineBytes: this.runtime.config.execution.maxInlineBytes });
|
|
11525
|
-
if (!parsed.ok) throw processError(result, parsed, this.accounts.remediation(plan.account, scopes));
|
|
11630
|
+
if (!parsed.ok) throw processError(result, parsed, this.accounts.remediation(plan.account, scopes, method?.service));
|
|
11526
11631
|
const delivered = stage && plan.files?.output ? await stage.deliver(plan.files.output, plan.files.limits) : void 0;
|
|
11527
11632
|
const file = delivered ? { path: delivered.path, bytes: delivered.bytes, sha256: delivered.sha256 } : void 0;
|
|
11528
11633
|
this.runtime.plans.complete(plan.id);
|
package/dist/tui.js
CHANGED
|
@@ -31,6 +31,7 @@ var account = object(
|
|
|
31
31
|
clientSource: { enum: CLIENT_SOURCES },
|
|
32
32
|
grantedScopes: texts,
|
|
33
33
|
missingScopes: texts,
|
|
34
|
+
delegationRequired: texts,
|
|
34
35
|
nextStep: text
|
|
35
36
|
},
|
|
36
37
|
["alias", "mode", "services", "connected"]
|
|
@@ -65,7 +66,7 @@ var isStrings = (value) => Array.isArray(value) && value.every((entry) => typeof
|
|
|
65
66
|
var optional = (value, check) => value === void 0 || check(value);
|
|
66
67
|
var isText = (value) => typeof value === "string";
|
|
67
68
|
function isAccount(value) {
|
|
68
|
-
return isRecord(value) && isText(value["alias"]) && MODES.includes(value["mode"]) && isStrings(value["services"]) && typeof value["connected"] === "boolean" && optional(value["integrationID"], isText) && optional(value["needsAuth"], (entry) => entry === true) && optional(value["access"], (entry) => ACCESS.includes(entry)) && optional(value["clientSource"], (entry) => CLIENT_SOURCES.includes(entry)) && optional(value["grantedScopes"], isStrings) && optional(value["missingScopes"], isStrings) && optional(value["nextStep"], isText);
|
|
69
|
+
return isRecord(value) && isText(value["alias"]) && MODES.includes(value["mode"]) && isStrings(value["services"]) && typeof value["connected"] === "boolean" && optional(value["integrationID"], isText) && optional(value["needsAuth"], (entry) => entry === true) && optional(value["access"], (entry) => ACCESS.includes(entry)) && optional(value["clientSource"], (entry) => CLIENT_SOURCES.includes(entry)) && optional(value["grantedScopes"], isStrings) && optional(value["missingScopes"], isStrings) && optional(value["delegationRequired"], isStrings) && optional(value["nextStep"], isText);
|
|
69
70
|
}
|
|
70
71
|
function isStatus(value) {
|
|
71
72
|
return isRecord(value) && isText(value["pluginVersion"]) && isText(value["hostVersion"]) && optional(value["hostWarning"], isText) && isRecord(value["gws"]) && isText(value["gws"]["path"]) && isText(value["gws"]["version"]) && isStrings(value["services"]) && Array.isArray(value["accounts"]) && value["accounts"].every(isAccount) && optional(value["auditDegraded"], (entry) => typeof entry === "boolean") && isStrings(value["unguardedAgents"]) && optional(value["permissionsBlock"], isText) && optional(value["nextStep"], isText);
|
|
@@ -78,6 +79,11 @@ function connectionLabel(account2) {
|
|
|
78
79
|
function openCodeLabel(status2) {
|
|
79
80
|
return status2.hostWarning ? `${status2.hostVersion} (host untested: ${status2.hostWarning})` : `${status2.hostVersion} (tested)`;
|
|
80
81
|
}
|
|
82
|
+
var DELEGATION_LABEL = "Needs domain-wide delegation";
|
|
83
|
+
function delegationText(account2) {
|
|
84
|
+
const services = account2.delegationRequired ?? [];
|
|
85
|
+
return `${services.join(", ")} (a Workspace admin sets it up; then add the scopes to accounts.${account2.alias}.scopes)`;
|
|
86
|
+
}
|
|
81
87
|
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.";
|
|
82
88
|
|
|
83
89
|
// src/tui.tsx
|
|
@@ -248,6 +254,23 @@ function AccountStatus(props) {
|
|
|
248
254
|
}
|
|
249
255
|
})
|
|
250
256
|
}), null);
|
|
257
|
+
_$insert(_el$4, _$createComponent(Show, {
|
|
258
|
+
get when() {
|
|
259
|
+
return _$memo(() => !!props.account.delegationRequired?.length)() ? props.account : void 0;
|
|
260
|
+
},
|
|
261
|
+
children: (account2) => _$createComponent(Row, {
|
|
262
|
+
label: DELEGATION_LABEL,
|
|
263
|
+
get value() {
|
|
264
|
+
return delegationText(account2());
|
|
265
|
+
},
|
|
266
|
+
get fg() {
|
|
267
|
+
return muted();
|
|
268
|
+
},
|
|
269
|
+
get muted() {
|
|
270
|
+
return muted();
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}), null);
|
|
251
274
|
return _el$4;
|
|
252
275
|
})();
|
|
253
276
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joe-broadhead/opencode-gws",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Unofficial OpenCode plugin for Google Workspace (gws CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"docs:coverage": "node scripts/generate-docs.mjs"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@joe-broadhead/opencode-gws-bin-darwin-arm64": "0.0.
|
|
54
|
-
"@joe-broadhead/opencode-gws-bin-darwin-x64": "0.0.
|
|
55
|
-
"@joe-broadhead/opencode-gws-bin-linux-arm64-gnu": "0.0.
|
|
56
|
-
"@joe-broadhead/opencode-gws-bin-linux-x64-gnu": "0.0.
|
|
53
|
+
"@joe-broadhead/opencode-gws-bin-darwin-arm64": "0.0.2",
|
|
54
|
+
"@joe-broadhead/opencode-gws-bin-darwin-x64": "0.0.2",
|
|
55
|
+
"@joe-broadhead/opencode-gws-bin-linux-arm64-gnu": "0.0.2",
|
|
56
|
+
"@joe-broadhead/opencode-gws-bin-linux-x64-gnu": "0.0.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/core": "7.29.7",
|
|
@@ -13,7 +13,7 @@ Call `gws_status` before the first Workspace call in a session. It lists account
|
|
|
13
13
|
|
|
14
14
|
Omit `account` when `gws_status` shows one account. Pass the alias when it shows more than one. A missing alias is `ACCOUNT_REQUIRED`. An unknown alias is `ACCOUNT_UNKNOWN`.
|
|
15
15
|
|
|
16
|
-
An account signs in Read-only or Read and write; `gws_status` shows which. The `default` account defaults to Read and write, which covers every method a Google sign-in can run. An operation whose scopes were not granted fails with `AUTH_SCOPE_MISSING` before anything runs: ask the user to sign in again, approve every scope Google lists, and choose Read and write if the error says so. Do not invent a scope. A method that `gws_catalog` or `gws_schema` marks `appOnly` needs a Chat app's or Classroom add-on's own credentials; no sign-in can run it, so use another method.
|
|
16
|
+
An account signs in Read-only or Read and write; `gws_status` shows which. The `default` account defaults to Read and write, which covers every method a Google sign-in can run. An operation whose scopes were not granted fails with `AUTH_SCOPE_MISSING` before anything runs: ask the user to sign in again, approve every scope Google lists, and choose Read and write if the error says so. Do not invent a scope. A method that `gws_catalog` or `gws_schema` marks `appOnly` (`restriction: "app-only"`) needs a Chat app's or Classroom add-on's own credentials; no sign-in can run it, so use another method. A method marked `restriction: "domain-wide-delegation"` (every Keep method) runs only when a Workspace admin has set up domain-wide delegation and `accounts.<alias>.scopes` adds a Keep scope; otherwise tell the user so.
|
|
17
17
|
|
|
18
18
|
Not signed in: `AUTH_REQUIRED`. The next step is to run `/connect` and choose Google Workspace, or `opencode auth login gws-<alias>`.
|
|
19
19
|
|
|
@@ -115,6 +115,7 @@ An error is `CODE: What happened. Next step: What to do.` Do the next step once.
|
|
|
115
115
|
| --- | --- |
|
|
116
116
|
| `AUTH_REQUIRED` | Ask the user to sign in as the message says, then retry. |
|
|
117
117
|
| `AUTH_SCOPE_MISSING` | Ask the user to sign in again with the access the message names, or to add the scope. Nothing ran. |
|
|
118
|
+
| `ADMIN_PRIVILEGE_REQUIRED` | The sign-in is valid; the account lacks the Workspace admin privilege the API needs. Do not ask the user to sign in again as the same account. Tell them the message's next step. |
|
|
118
119
|
| `API_DISABLED` | Ask the user to enable the API at the URL in the message, wait about a minute, and retry. |
|
|
119
120
|
| `OPERATION_UNKNOWN` | The id is not in the catalog. Search again with `gws_catalog`. |
|
|
120
121
|
| `HELPER_UNSUPPORTED` | The helper cannot run. Use the method `gws_catalog` names as `alternative`. |
|
|
@@ -26,7 +26,7 @@ Plugin policy cannot allow an admin call. A policy rule can only deny.
|
|
|
26
26
|
|
|
27
27
|
## Admin reports
|
|
28
28
|
|
|
29
|
-
Read-only at both access levels. The sign-in still needs the admin report scopes from the auth doc.
|
|
29
|
+
Read-only at both access levels. The sign-in still needs the admin report scopes from the auth doc, and the account must be a Workspace admin with the Reports privilege: otherwise every call fails with `ADMIN_PRIVILEGE_REQUIRED`, which signing in again as the same account does not fix.
|
|
30
30
|
|
|
31
31
|
Activity list, read. `fields` is required because this is a list:
|
|
32
32
|
|