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
49 changes: 49 additions & 0 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,43 @@ expand: false
---
```

#### 5.2.13 `allowed_surfaces` (optional)
- **Type:** Array of strings
- **Purpose:** Restrict task execution to matching invocation surfaces
- **Default:** Unrestricted when omitted or empty
- **Matching:** Case-insensitive OR logic across entries

```yaml
---
allowed_surfaces:
- jira
- github
---
```

The caller must configure its invocation surface with `WithSurface` or the
`-surface` CLI flag. A restricted task rejects invocations with a missing or
non-matching surface before task content is assembled.

#### 5.2.14 `allowed_requesters` (optional)
- **Type:** Array of strings
- **Purpose:** Restrict task execution to matching requester usernames or email addresses
- **Default:** Unrestricted when omitted or empty
- **Matching:** Case-insensitive OR logic across entries

```yaml
---
allowed_requesters:
- alice
- alice@example.com
---
```

The caller must configure a requester with `WithCaller` or the `-caller` and
`-caller-email` CLI flags. A username or email match permits the requester.
When both `allowed_surfaces` and `allowed_requesters` are present, both
restrictions must match.

### 5.3 Standard Rule Fields

#### 5.3.1 `id` (optional)
Expand Down Expand Up @@ -1554,6 +1591,16 @@ Potential future additions while maintaining backward compatibility:
"description": "Enable/disable parameter expansion",
"default": true
},
"allowed_surfaces": {
"type": "array",
"items": {"type": "string"},
"description": "Invocation surfaces permitted to run a task"
},
"allowed_requesters": {
"type": "array",
"items": {"type": "string"},
"description": "Requester usernames or email addresses permitted to run a task"
},
"selectors": {
"type": "object",
"description": "Auto-filter rules",
Expand Down Expand Up @@ -1634,6 +1681,8 @@ Potential future additions while maintaining backward compatibility:
| `timeout` | string | No | Timeout duration |
| `selectors` | object | No | Auto-filter rules |
| `expand` | boolean | No | Parameter expansion control |
| `allowed_surfaces` | array | No | Permitted invocation surfaces |
| `allowed_requesters` | array | No | Permitted requester usernames or email addresses |

### Rule Fields

Expand Down
39 changes: 39 additions & 0 deletions docs/how-to/create-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,45 @@ coding-context -s priority=high implement-feature

**Note:** Language values should be lowercase (e.g., `go`, `python`, `javascript`).

## Restricting Task Invocation

Use `allowed_surfaces` and `allowed_requesters` to limit who may run a task and
where the invocation may originate:

```markdown
---
allowed_surfaces:
- jira
- github
allowed_requesters:
- alice
- alice@example.com
---
# Deploy Service

Deploy the requested service update.
```

Values within each list use case-insensitive OR logic. When both fields are
present, the invocation must match both restrictions. Omitting a field or
setting it to an empty list leaves that dimension unrestricted.

Library callers provide trusted invocation data with `WithSurface` and
`WithCaller`. Direct CLI callers use `-surface`, `-caller`, and optionally
`-caller-email`:

```bash
coding-context \
-surface jira \
-caller alice \
-caller-email alice@example.com \
deploy-service
```

Restricted tasks reject missing or non-matching invocation data before task
content is assembled. Linting validates the task file without enforcing
invocation restrictions.

## Task Frontmatter

Task frontmatter is used for filtering and metadata purposes but is **NOT included in the output**. The frontmatter controls behavior (like auto-filtering rules via the `selectors` field) but only the task content below the frontmatter delimiters is included in the assembled context.
Expand Down
37 changes: 36 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ coding-context \

### `-a <agent>`

**Type:** String
**Type:** String
**Default:** (empty)

Specify the target agent being used. This is currently used for:
Expand Down Expand Up @@ -216,6 +216,41 @@ coding-context -a copilot -w implement-feature

**See also:** [Targeting a Specific Agent](../../README.md#targeting-a-specific-agent) in README

### `-surface <surface>`

**Type:** String
**Default:** (empty)

Set the invocation surface used to authorize a task that declares
`allowed_surfaces`. Matching is case-insensitive. A restricted task rejects an
empty or non-matching surface.

### `-caller <username>`

**Type:** String
**Default:** (empty)

Set the requester username used to authorize a task that declares
`allowed_requesters`.

### `-caller-email <email>`

**Type:** String
**Default:** (empty)

Set the requester email address used to authorize a task that declares
`allowed_requesters`. Either the configured username or email may match an
allowed requester.

**Example:**
```bash
coding-context \
-surface jira \
-caller alice \
-caller-email alice@example.com \
deploy-service
```

### `-r`

**Type:** Boolean flag
Expand Down
20 changes: 15 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import (
)

var (
errInvalidUsage = errors.New("invalid usage: expected one task name argument and optional user-prompt")
errWriteRulesNoAgent = errors.New("-w flag requires an agent to be specified (via task 'agent' field or -a flag)")
errNoUserRulePath = errors.New("no user rule path available for agent")
errRulesPathEscapesHome = errors.New("rules path escapes home directory")
errAgentFlagsMutExcl = errors.New("-a and -A flags are mutually exclusive")
errInvalidUsage = errors.New("invalid usage: expected one task name argument and optional user-prompt")
errWriteRulesNoAgent = errors.New("-w flag requires an agent to be specified (via task 'agent' field or -a flag)")
errNoUserRulePath = errors.New("no user rule path available for agent")
errRulesPathEscapesHome = errors.New("rules path escapes home directory")
errAgentFlagsMutExcl = errors.New("-a and -A flags are mutually exclusive")
)

type cliConfig struct {
Expand All @@ -37,6 +37,8 @@ type cliConfig struct {
searchPaths []string
lenientSearchPaths []string
manifestURL string
surface string
caller codingcontext.CallerIdentity
taskName string
userPrompt string
}
Expand Down Expand Up @@ -81,6 +83,8 @@ func run(ctx context.Context, logger *slog.Logger) error {
codingcontext.WithUserPrompt(cfg.userPrompt),
codingcontext.WithAgent(cfg.agent),
codingcontext.WithLenientAgent(cfg.lenientAgent),
codingcontext.WithSurface(cfg.surface),
codingcontext.WithCaller(cfg.caller),
)

result, err := cc.Run(ctx, cfg.taskName)
Expand Down Expand Up @@ -156,6 +160,12 @@ func parseFlags(logger *slog.Logger) (*cliConfig, error) {
})
flag.StringVar(&cfg.manifestURL, "m", "",
"Go Getter URL to a manifest file containing search paths (one per line). Every line is included as-is.")
flag.StringVar(&cfg.surface, "surface", "",
"Invocation surface used to authorize tasks that declare allowed_surfaces.")
flag.StringVar(&cfg.caller.Username, "caller", "",
"Requester username used to authorize tasks that declare allowed_requesters.")
flag.StringVar(&cfg.caller.Email, "caller-email", "",
"Requester email used to authorize tasks that declare allowed_requesters.")

setupUsage(logger)
flag.Parse()
Expand Down
47 changes: 47 additions & 0 deletions pkg/codingcontext/authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package codingcontext

import (
"fmt"
"strings"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/markdown"
)

// CallerIdentity identifies the requester invoking a task.
type CallerIdentity struct {
Username string
Email string
}

func (cc *Context) authorizeTaskInvocation(frontMatter markdown.TaskFrontMatter) error {
if !isAllowed(frontMatter.AllowedSurfaces, cc.surface) {
return fmt.Errorf("%w: surface is not permitted", ErrCallerNotAllowed)
}

if !isAllowed(frontMatter.AllowedRequesters, cc.caller.Username, cc.caller.Email) {
return fmt.Errorf("%w: requester identity is not permitted", ErrCallerNotAllowed)
}

return nil
}

func isAllowed(allowlist []string, values ...string) bool {
if len(allowlist) == 0 {
return true
}

for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}

for _, allowed := range allowlist {
if strings.EqualFold(value, strings.TrimSpace(allowed)) {
return true
}
}
}

return false
}
Loading
Loading