| @@ -109,23 +109,23 @@ func Dispatch(c *Ctx, argv []string) int { |
| 109 | 109 | // A runner-scoped key reaches the runner protocol and nothing else, so |
| 110 | 110 | // the key a CI host holds cannot administer the instance. |
| 111 | 111 | if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { |
| 112 | | return c.fail(protocol.ExitDenied, "this key's scope (%s) does not allow control commands", c.Scope) |
| 112 | return c.fail(protocol.ExitDenied, "this key's scope (%s) does not allow control commands; use a key added with --scope full", c.Scope) |
| 113 | 113 | } |
| 114 | 114 | if c.ViaAPI && cmd.SSHOnly { |
| 115 | 115 | return c.fail(protocol.ExitDenied, "%s is only available over SSH", joinPath(cmd.Path)) |
| 116 | 116 | } |
| 117 | 117 | if c.ReadOnly && !cmd.ReadOnly { |
| 118 | | return c.fail(protocol.ExitDenied, "this token is read-only; %s modifies state", joinPath(cmd.Path)) |
| 118 | return c.fail(protocol.ExitDenied, "this token is read-only; %s modifies state — mint one with --scope full", joinPath(cmd.Path)) |
| 119 | 119 | } |
| 120 | 120 | // The SSH listener refuses a disabled account before it gets here; the |
| 121 | 121 | // API and the web reach Dispatch directly, so the check lives here too. |
| 122 | 122 | if c.User.Disabled { |
| 123 | | return c.fail(protocol.ExitDenied, "this account is disabled") |
| 123 | return c.fail(protocol.ExitDenied, "this account is disabled; ask an instance admin to enable it") |
| 124 | 124 | } |
| 125 | 125 | // The admin noun is gated here as well as in each handler, so a new |
| 126 | 126 | // admin command that forgets requireInstanceAdmin is still refused. |
| 127 | 127 | if cmd.Path[0] == "admin" && !c.User.IsAdmin { |
| 128 | | return c.fail(protocol.ExitDenied, "admin commands are for instance admins") |
| 128 | return c.fail(protocol.ExitDenied, "admin commands are for instance admins; ask one") |
| 129 | 129 | } |
| 130 | 130 | if c.User.Pending && !pendingAllowed(cmd.Path) { |
| 131 | 131 | return c.fail(protocol.ExitDenied, |
| @@ -218,9 +218,16 @@ func (emptyReader) Read([]byte) (int, error) { return 0, io.EOF } |
| 218 | 218 | // otherwise via the plain formatter. |
| 219 | 219 | func (c *Ctx) emit(data any, plain func(w io.Writer)) int { |
| 220 | 220 | // A nil slice would serialize as null; consumers should see []. |
| 221 | | if v := reflect.ValueOf(data); v.Kind() == reflect.Slice && v.IsNil() { |
| 221 | v := reflect.ValueOf(data) |
| 222 | if v.Kind() == reflect.Slice && v.IsNil() { |
| 222 | 223 | data = reflect.MakeSlice(v.Type(), 0, 0).Interface() |
| 223 | 224 | } |
| 225 | // An empty list prints nothing a script would read; the person at |
| 226 | // the terminal hears about it on stderr. |
| 227 | if !c.JSON && v.Kind() == reflect.Slice && v.Len() == 0 { |
| 228 | fmt.Fprintln(c.Stderr, "nothing to list") |
| 229 | return protocol.ExitOK |
| 230 | } |
| 224 | 231 | if c.JSON { |
| 225 | 232 | enc := json.NewEncoder(c.Stdout) |
| 226 | 233 | enc.SetEscapeHTML(false) |