From 43f4a2845bf02f4f4041125c6a7549db800ffbe1 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 21:35:00 +0000 Subject: [PATCH] feat(addons): declare the connector file operations in Slack and Telegram The two connectors move files through connector.file.get and connector.file.put but their manifests asked for nothing, so the install dialog and blanket approval could not cover them. Declare both as optional: a connector works without files. Delivery is pull, so neither calls connector.inbound and it is not declared. The audit of official packages now reads the operation constants of the shared connector code. A deployment whose policy file already grants the connector prefix sees no change. Assisted-by: Claude:claude-sonnet-5-5 go --- addons/slack/manifest.json | 3 + addons/telegram/manifest.json | 3 + .../packaging/official_contract_test.go | 74 +++++++++++++------ 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/addons/slack/manifest.json b/addons/slack/manifest.json index 6fb07352fb..7effd61047 100644 --- a/addons/slack/manifest.json +++ b/addons/slack/manifest.json @@ -10,6 +10,9 @@ "protocol": { "min": 1, "max": 1 }, "dataFormat": { "min": 1, "max": 1 }, "capabilities": { + "hostOperations": { + "optional": ["connector.file.get", "connector.file.put"] + }, "connector": { "kinds": [ { diff --git a/addons/telegram/manifest.json b/addons/telegram/manifest.json index eb02d34ae6..1a682aaa8a 100644 --- a/addons/telegram/manifest.json +++ b/addons/telegram/manifest.json @@ -10,6 +10,9 @@ "protocol": { "min": 1, "max": 1 }, "dataFormat": { "min": 1, "max": 1 }, "capabilities": { + "hostOperations": { + "optional": ["connector.file.get", "connector.file.put"] + }, "connector": { "kinds": [ { diff --git a/pkg/addons/packaging/official_contract_test.go b/pkg/addons/packaging/official_contract_test.go index 2cf5dfc904..b5dfc4b5c2 100644 --- a/pkg/addons/packaging/official_contract_test.go +++ b/pkg/addons/packaging/official_contract_test.go @@ -27,6 +27,19 @@ var officialRequests = map[string]contract.HostOperations{ "office": {Required: strings.Fields("workspace.export workspace.import")}, "sandbox": {Required: strings.Fields("workspace.changes workspace.commit workspace.materialize"), Optional: strings.Fields("egress.audit egress.check workspace.admin")}, "web": {Optional: []string{"workspace.admin"}}, + // A connector moves files only when the person turns files on, so both + // file operations are optional. Delivery is pull, so neither add-on calls + // connector.inbound. + "slack": {Optional: strings.Fields("connector.file.get connector.file.put")}, + "telegram": {Optional: strings.Fields("connector.file.get connector.file.put")}, +} + +// connectorRoot holds the file transfer code that slack and telegram share. It +// names its operations by constant, so the audit reads those too. +const connectorRoot = "addons/internal/connector" + +var connectorConstants = map[string]string{ + "ConnectorInboundOp": contract.ConnectorInboundOp, "ConnectorFilePutOp": contract.ConnectorFilePutOp, "ConnectorFileGetOp": contract.ConnectorFileGetOp, } var _ = Describe("official typed package contracts", func() { @@ -47,31 +60,44 @@ var _ = Describe("official typed package contracts", func() { // passed to jobundo. An unknown operation-shaped literal is not evidence // of support; compare independently with the real registered vocabulary. var used []string - Expect(filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { - return nil - } - f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) - if err != nil { - return err - } - ast.Inspect(f, func(n ast.Node) bool { - lit, ok := n.(*ast.BasicLit) - if !ok || lit.Kind != token.STRING { + roots := []string{root} + if id == "slack" || id == "telegram" { + roots = append(roots, filepath.Join("../../..", connectorRoot)) + } + for _, root := range roots { + Expect(filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + return err + } + ast.Inspect(f, func(n ast.Node) bool { + if sel, ok := n.(*ast.SelectorExpr); ok { + if pkg, ok := sel.X.(*ast.Ident); ok && pkg.Name == "contract" { + if value, ok := connectorConstants[sel.Sel.Name]; ok { + used = append(used, value) + } + } + } + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + value, err := strconv.Unquote(lit.Value) + Expect(err).NotTo(HaveOccurred()) + if slices.Contains(hostops.SupportedOperations(), value) { + used = append(used, value) + } return true - } - value, err := strconv.Unquote(lit.Value) - Expect(err).NotTo(HaveOccurred()) - if slices.Contains(hostops.SupportedOperations(), value) { - used = append(used, value) - } - return true - }) - return nil - })).To(Succeed()) + }) + return nil + })).To(Succeed()) + } declared := append(slices.Clone(want.Required), want.Optional...) slices.Sort(used) used = slices.Compact(used)