Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 0 additions & 57 deletions .github/workflows/claude-code-review.yml

This file was deleted.

50 changes: 0 additions & 50 deletions .github/workflows/claude.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Thumbs.db
# AI assistant
CLAUDE.md
AGENTS.md
.opencode/
.serena/
.sisyphus/

Expand Down
2 changes: 1 addition & 1 deletion custom/ec2/instances/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func init() {
Name: "SSM Session",
Shortcut: "x",
Type: action.ActionTypeExec,
Command: "aws ssm start-session --target ${ID}",
Args: []string{"aws", "ssm", "start-session", "--target", "${ID}"},
},
})

Expand Down
15 changes: 3 additions & 12 deletions custom/rds/instances/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

rdsClient "github.com/clawscli/claws/custom/rds"
"github.com/clawscli/claws/internal/action"
appaws "github.com/clawscli/claws/internal/aws"
"github.com/clawscli/claws/internal/dao"
)

Expand Down Expand Up @@ -152,22 +151,14 @@ func executeDeleteInstance(ctx context.Context, resource dao.Resource) action.Ac
return action.InvalidResourceResult()
}

client, err := rdsClient.GetClient(ctx)
d, err := NewInstanceDAO(ctx)
if err != nil {
return action.ActionResult{Success: false, Error: err}
}

identifier := instance.GetID()
skipFinalSnapshot := true
input := &rds.DeleteDBInstanceInput{
DBInstanceIdentifier: &identifier,
SkipFinalSnapshot: &skipFinalSnapshot,
DeleteAutomatedBackups: appaws.BoolPtr(true),
}

_, err = client.DeleteDBInstance(ctx, input)
if err != nil {
return action.ActionResult{Success: false, Error: fmt.Errorf("delete db instance: %w", err)}
if err := d.Delete(ctx, identifier); err != nil {
return action.ActionResult{Success: false, Error: err}
}

return action.ActionResult{
Expand Down
5 changes: 4 additions & 1 deletion custom/vpc/tgw-attachments/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ func (d *TGWAttachmentDAO) Delete(ctx context.Context, id string) error {
if err != nil {
return err
}
attRes := att.(*TGWAttachmentResource)
attRes, ok := att.(*TGWAttachmentResource)
if !ok {
return fmt.Errorf("unexpected transit gateway attachment resource type %T", att)
}

switch attRes.ResourceType() {
case "vpc":
Expand Down
4 changes: 2 additions & 2 deletions custom/vpc/vpcs/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ func TestIntegration_VPCDAO_ServiceInfo(t *testing.T) {
t.Fatalf("Failed to create VPCDAO: %v", err)
}

if dao.ServiceName() != "ec2" {
t.Errorf("ServiceName() = %q, want %q", dao.ServiceName(), "ec2")
if dao.ServiceName() != "vpc" {
t.Errorf("ServiceName() = %q, want %q", dao.ServiceName(), "vpc")
}
if dao.ResourceType() != "vpcs" {
t.Errorf("ResourceType() = %q, want %q", dao.ResourceType(), "vpcs")
Expand Down
35 changes: 29 additions & 6 deletions custom/wafv2/web-acls/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package webacls

import (
"context"
"errors"
"fmt"

"github.com/aws/aws-sdk-go-v2/service/wafv2"
"github.com/aws/aws-sdk-go-v2/service/wafv2/types"

appaws "github.com/clawscli/claws/internal/aws"
appconfig "github.com/clawscli/claws/internal/config"
"github.com/clawscli/claws/internal/dao"
apperrors "github.com/clawscli/claws/internal/errors"
)

const cloudFrontWebACLRegion = "us-east-1"

// WebACLDAO provides data access for WAFv2 Web ACLs
type WebACLDAO struct {
dao.BaseDAO
Expand Down Expand Up @@ -41,19 +45,26 @@ func (d *WebACLDAO) List(ctx context.Context) ([]dao.Resource, error) {
}
resources = append(resources, regionalResources...)

// List CLOUDFRONT Web ACLs (only available in us-east-1)
// We'll try to list CloudFront scope but it may fail if not in us-east-1
// List CLOUDFRONT Web ACLs only from us-east-1, where WAFv2 exposes CloudFront scope.
if currentRegion(ctx) != cloudFrontWebACLRegion {
return resources, nil
}
cloudfrontResources, err := d.listByScope(ctx, types.ScopeCloudfront)
if err != nil {
// CloudFront scope may fail if not in us-east-1, ignore this error
// and just return regional resources
return resources, nil
return resources, fmt.Errorf("list cloudfront web acls: %w", err)
}
resources = append(resources, cloudfrontResources...)

return resources, nil
}

func currentRegion(ctx context.Context) string {
if region := appaws.GetRegionFromContext(ctx); region != "" {
return region
}
return appconfig.Global().Region()
}

func (d *WebACLDAO) listByScope(ctx context.Context, scope types.Scope) ([]dao.Resource, error) {
acls, err := appaws.Paginate(ctx, func(token *string) ([]types.WebACLSummary, *string, error) {
output, err := d.client.ListWebACLs(ctx, &wafv2.ListWebACLsInput{
Expand Down Expand Up @@ -82,9 +93,11 @@ func (d *WebACLDAO) listByScope(ctx context.Context, scope types.Scope) ([]dao.R
func (d *WebACLDAO) Get(ctx context.Context, id string) (dao.Resource, error) {
// Parse the composite ID (scope/name/id)
// For simplicity, we'll search through both scopes
for _, scope := range []types.Scope{types.ScopeRegional, types.ScopeCloudfront} {
var scopeErrs []error
for _, scope := range d.scopes(ctx) {
resources, err := d.listByScope(ctx, scope)
if err != nil {
scopeErrs = append(scopeErrs, fmt.Errorf("list %s web acls: %w", scope, err))
continue
}

Expand All @@ -98,9 +111,19 @@ func (d *WebACLDAO) Get(ctx context.Context, id string) (dao.Resource, error) {
}
}

if len(scopeErrs) > 0 {
return nil, errors.Join(append([]error{fmt.Errorf("web acl %s not found", id)}, scopeErrs...)...)
}
return nil, fmt.Errorf("web acl %s not found", id)
}

func (d *WebACLDAO) scopes(ctx context.Context) []types.Scope {
if currentRegion(ctx) == cloudFrontWebACLRegion {
return []types.Scope{types.ScopeRegional, types.ScopeCloudfront}
}
return []types.Scope{types.ScopeRegional}
}

func (d *WebACLDAO) getWebACLDetail(ctx context.Context, summary *WebACLResource) (*WebACLResource, error) {
input := &wafv2.GetWebACLInput{
Name: summary.Summary.Name,
Expand Down
90 changes: 90 additions & 0 deletions custom/wafv2/web-acls/dao_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package webacls

import (
"bytes"
"context"
"io"
"net/http"
"strings"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/wafv2"

appaws "github.com/clawscli/claws/internal/aws"
)

func TestListSkipsCloudFrontScopeOutsideUSEast1(t *testing.T) {
client := &recordingHTTPClient{}
d := newTestWebACLDAO(client)

_, err := d.List(appaws.WithRegionOverride(context.Background(), "us-west-2"))
if err != nil {
t.Fatalf("List() returned error: %v", err)
}

if got := client.scopeCalls("REGIONAL"); got != 1 {
t.Fatalf("REGIONAL calls = %d, want 1; bodies=%v", got, client.bodies)
}
if got := client.scopeCalls("CLOUDFRONT"); got != 0 {
t.Fatalf("CLOUDFRONT calls = %d, want 0; bodies=%v", got, client.bodies)
}
}

func TestListIncludesCloudFrontScopeInUSEast1(t *testing.T) {
client := &recordingHTTPClient{}
d := newTestWebACLDAO(client)

_, err := d.List(appaws.WithRegionOverride(context.Background(), cloudFrontWebACLRegion))
if err != nil {
t.Fatalf("List() returned error: %v", err)
}

if got := client.scopeCalls("REGIONAL"); got != 1 {
t.Fatalf("REGIONAL calls = %d, want 1; bodies=%v", got, client.bodies)
}
if got := client.scopeCalls("CLOUDFRONT"); got != 1 {
t.Fatalf("CLOUDFRONT calls = %d, want 1; bodies=%v", got, client.bodies)
}
}

func newTestWebACLDAO(httpClient aws.HTTPClient) *WebACLDAO {
return &WebACLDAO{
client: wafv2.NewFromConfig(aws.Config{
Region: cloudFrontWebACLRegion,
HTTPClient: httpClient,
Credentials: aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
return aws.Credentials{AccessKeyID: "test", SecretAccessKey: "test", Source: "test"}, nil
}),
}),
}
}

type recordingHTTPClient struct {
bodies []string
}

func (c *recordingHTTPClient) Do(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
c.bodies = append(c.bodies, string(body))

return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/x-amz-json-1.1"}},
Body: io.NopCloser(bytes.NewBufferString(`{"WebACLs":[]}`)),
}, nil
}

func (c *recordingHTTPClient) scopeCalls(scope string) int {
needle := `"Scope":"` + scope + `"`
count := 0
for _, body := range c.bodies {
if strings.Contains(body, needle) {
count++
}
}
return count
}
4 changes: 3 additions & 1 deletion docs/ai-chat.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ ai:
- サービスやリージョンをまたいでAWSリソースの一覧取得やクエリを実行
- 特定のリソースの詳細情報を取得
- 対応リソース(Lambda、ECS、CodeBuildなど)のCloudWatchログを取得
- AWSドキュメントを検索
- 一般的で公開可能なAWS用語でAWSドキュメントを検索

AIは現在のプロファイル、リージョン、リソースコンテキストを自動的に使用します。

AWSドキュメント検索では、検索クエリがAWSの公開ドキュメント検索エンドポイントへ送信されます。プライベートなAWSコンテキストの漏えいを避けるため、clawsはリソースID、アカウントID、ARN、プロファイル名、ログ行、タグ値、シークレットを含むドキュメント検索クエリを拒否します。

### コンテキスト認識

アシスタントは現在のビューに基づいてコンテキストを自動的に受け取ります:
Expand Down
4 changes: 3 additions & 1 deletion docs/ai-chat.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ ai:
- 서비스 및 리전에 걸쳐 AWS 리소스 목록 조회 및 쿼리 실행
- 특정 리소스의 상세 정보 가져오기
- 지원 리소스(Lambda, ECS, CodeBuild 등)의 CloudWatch 로그 가져오기
- AWS 문서 검색
- 일반적이고 공개 가능한 AWS 용어로 AWS 문서 검색

AI는 현재 프로필, 리전, 리소스 컨텍스트를 자동으로 사용합니다.

AWS 문서 검색은 검색 쿼리를 AWS의 공개 문서 검색 엔드포인트로 전송합니다. 비공개 AWS 컨텍스트가 유출되지 않도록 claws는 리소스 ID, 계정 ID, ARN, 프로필 이름, 로그 행, 태그 값 또는 시크릿이 포함된 문서 검색 쿼리를 거부합니다.

### 컨텍스트 인식

어시스턴트는 현재 뷰에 따라 컨텍스트를 자동으로 수신합니다:
Expand Down
Loading
Loading