fix(mcp): allow the stall watchdog to archive its own task (#3260)
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/kandev/kandev/internal/task/models"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
ws "github.com/kandev/kandev/pkg/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestHandleArchiveTask_ScheduledAutomationSelfArchiveSucceeds pins that
|
||||
// validateAutomationArchiveTarget's binding check, scoped to
|
||||
// TriggerTypeGitHubPRMerged callers, does not block a scheduled automation
|
||||
// from archiving its own hidden task at this second guard.
|
||||
func TestHandleArchiveTask_ScheduledAutomationSelfArchiveSucceeds(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-self", Name: "Self"}))
|
||||
require.NoError(t, repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-self", WorkspaceID: "ws-self", Name: "Board"}))
|
||||
caller := &models.Task{
|
||||
ID: "automation-run", WorkspaceID: "ws-self", WorkflowID: "wf-self",
|
||||
Title: "Scheduled run", State: v1.TaskStateTODO,
|
||||
Origin: models.TaskOriginAutomationRun,
|
||||
Metadata: map[string]interface{}{"trigger_type": "scheduled"},
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, caller))
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
msg := makeWSMessage(t, ws.ActionMCPArchiveTask, map[string]string{
|
||||
"task_id": caller.ID, "caller_task_id": caller.ID,
|
||||
})
|
||||
resp, err := h.handleArchiveTask(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ws.MessageTypeResponse, resp.Type)
|
||||
archived, err := svc.GetTask(ctx, caller.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, archived.ArchivedAt)
|
||||
}
|
||||
|
||||
// TestHandleArchiveTask_MergedPRRunRejectsSelfArchive pins that a
|
||||
// github_pr_merged automation cannot archive its own run task in place of
|
||||
// its bound PR target. The binding check in validateAutomationArchiveTarget
|
||||
// is scoped to TriggerTypeGitHubPRMerged callers, so the run task ID never
|
||||
// equals the expected PR target and the guard rejects it.
|
||||
func TestHandleArchiveTask_MergedPRRunRejectsSelfArchive(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-self-pr", Name: "Self PR"}))
|
||||
require.NoError(t, repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-self-pr", WorkspaceID: "ws-self-pr", Name: "Board"}))
|
||||
prTarget := &models.Task{
|
||||
ID: "pr-target", WorkspaceID: "ws-self-pr", WorkflowID: "wf-self-pr",
|
||||
Title: "PR task", State: v1.TaskStateTODO,
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, prTarget))
|
||||
caller := &models.Task{
|
||||
ID: "automation-run", WorkspaceID: "ws-self-pr", WorkflowID: "wf-self-pr",
|
||||
Title: "PR merged run", State: v1.TaskStateTODO,
|
||||
Origin: models.TaskOriginAutomationRun,
|
||||
Metadata: map[string]interface{}{
|
||||
"trigger_type": "github_pr_merged",
|
||||
models.MetaKeyAutomationTargetTaskID: prTarget.ID,
|
||||
},
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, caller))
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
msg := makeWSMessage(t, ws.ActionMCPArchiveTask, map[string]string{
|
||||
"task_id": caller.ID, "caller_task_id": caller.ID,
|
||||
})
|
||||
resp, err := h.handleArchiveTask(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
assertWSError(t, resp, ws.ErrorCodeValidation)
|
||||
unchanged, err := svc.GetTask(ctx, caller.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, unchanged.ArchivedAt, "pr_merged run must not self-archive when bound to a different target")
|
||||
}
|
||||
@@ -59,11 +59,14 @@ var automationSurfaceActions = map[string]struct{}{
|
||||
ws.ActionMCPResolveAgentPermission: {},
|
||||
}
|
||||
|
||||
// ActionMCPArchiveTask is deliberately absent: a hidden automation_run task
|
||||
// archiving itself is its normal end-of-run completion signal, not the kind
|
||||
// of self-mutation (messaging, stopping, spawning a session on itself) this
|
||||
// list exists to block.
|
||||
var automationSelfDeniedActions = map[string]struct{}{
|
||||
ws.ActionMCPCreateTask: {},
|
||||
ws.ActionMCPUpdateTask: {},
|
||||
ws.ActionMCPMoveTask: {},
|
||||
ws.ActionMCPArchiveTask: {},
|
||||
ws.ActionMCPAddTaskDependency: {},
|
||||
ws.ActionMCPRemoveTaskDependency: {},
|
||||
ws.ActionMCPMessageTask: {},
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mcpprofile "github.com/kandev/kandev/internal/mcp/profile"
|
||||
mcpscope "github.com/kandev/kandev/internal/mcp/scope"
|
||||
"github.com/kandev/kandev/internal/task/models"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
ws "github.com/kandev/kandev/pkg/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAuthorizeAutomationRequest_SelfArchiveSucceeds is the regression guard
|
||||
// for the Stall Session Watchdog defect: a hidden automation_run task
|
||||
// archiving itself (its STEP 5 completion signal) must not be rejected by
|
||||
// the automation self-target guard. Before the fix, ActionMCPArchiveTask was
|
||||
// listed in automationSelfDeniedActions and this returned a NOT_FOUND guard
|
||||
// response for every watchdog run, so its task was never archived.
|
||||
func TestAuthorizeAutomationRequest_SelfArchiveSucceeds(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
workspaces, err := svc.ListWorkspaces(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
workspaceID := workspaces[0].ID
|
||||
|
||||
now := time.Now().UTC()
|
||||
selfTask := &models.Task{
|
||||
ID: "watchdog-self-task",
|
||||
WorkspaceID: workspaceID,
|
||||
Title: "Watchdog run",
|
||||
State: v1.TaskStateInProgress,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, selfTask))
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
ctx = mcpscope.WithPrincipal(ctx, mcpscope.Principal{
|
||||
AutomationID: "automation-watchdog",
|
||||
WorkspaceID: workspaceID,
|
||||
CallerTaskID: selfTask.ID,
|
||||
CallerSessionID: "automation-session",
|
||||
Surface: mcpprofile.SurfaceAutomation,
|
||||
})
|
||||
msg := makeWSMessage(t, ws.ActionMCPArchiveTask, map[string]interface{}{
|
||||
"task_id": selfTask.ID,
|
||||
})
|
||||
|
||||
guarded, replacement, err := h.authorizeAutomationRequest(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, guarded, "self-archive must not be rejected by the automation guard")
|
||||
require.NotNil(t, replacement)
|
||||
}
|
||||
|
||||
// TestAuthorizeAutomationRequest_SelfCoordinationActionsStillDenied proves
|
||||
// the fix is scoped to archive only: a hidden automation task must still be
|
||||
// blocked from messaging, stopping, or spawning a session on itself.
|
||||
func TestAuthorizeAutomationRequest_SelfCoordinationActionsStillDenied(t *testing.T) {
|
||||
actions := []string{
|
||||
ws.ActionMCPMessageTask,
|
||||
ws.ActionMCPStopTask,
|
||||
ws.ActionMCPSpawnSession,
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
t.Run(action, func(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
workspaces, err := svc.ListWorkspaces(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
workspaceID := workspaces[0].ID
|
||||
|
||||
now := time.Now().UTC()
|
||||
selfTask := &models.Task{
|
||||
ID: "watchdog-self-task-" + action,
|
||||
WorkspaceID: workspaceID,
|
||||
Title: "Watchdog run",
|
||||
State: v1.TaskStateInProgress,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, selfTask))
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
ctx = mcpscope.WithPrincipal(ctx, mcpscope.Principal{
|
||||
AutomationID: "automation-watchdog",
|
||||
WorkspaceID: workspaceID,
|
||||
CallerTaskID: selfTask.ID,
|
||||
CallerSessionID: "automation-session",
|
||||
Surface: mcpprofile.SurfaceAutomation,
|
||||
})
|
||||
msg := makeWSMessage(t, action, map[string]interface{}{
|
||||
"task_id": selfTask.ID,
|
||||
})
|
||||
|
||||
guarded, _, err := h.authorizeAutomationRequest(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
assertWSError(t, guarded, ws.ErrorCodeNotFound)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAuthorizeAutomationRequest_CrossWorkspaceTaskStillDenied pins Defect
|
||||
// 1's boundary (unrelated to this fix, and must not regress): an automation
|
||||
// principal cannot target a task outside its own workspace, even for an
|
||||
// action that is now allowed against itself.
|
||||
func TestAuthorizeAutomationRequest_CrossWorkspaceTaskStillDenied(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
workspaces, err := svc.ListWorkspaces(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
homeWorkspaceID := workspaces[0].ID
|
||||
|
||||
now := time.Now().UTC()
|
||||
foreignWorkspace := &models.Workspace{
|
||||
ID: "ws-foreign-automation",
|
||||
Name: "Foreign",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
require.NoError(t, repo.CreateWorkspace(ctx, foreignWorkspace))
|
||||
foreignTask := &models.Task{
|
||||
ID: "foreign-task",
|
||||
WorkspaceID: foreignWorkspace.ID,
|
||||
Title: "Foreign task",
|
||||
State: v1.TaskStateInProgress,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
require.NoError(t, repo.CreateTask(ctx, foreignTask))
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
ctx = mcpscope.WithPrincipal(ctx, mcpscope.Principal{
|
||||
AutomationID: "automation-watchdog",
|
||||
WorkspaceID: homeWorkspaceID,
|
||||
CallerTaskID: "watchdog-self-task",
|
||||
CallerSessionID: "automation-session",
|
||||
Surface: mcpprofile.SurfaceAutomation,
|
||||
})
|
||||
msg := makeWSMessage(t, ws.ActionMCPArchiveTask, map[string]interface{}{
|
||||
"task_id": foreignTask.ID,
|
||||
})
|
||||
|
||||
guarded, _, err := h.authorizeAutomationRequest(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
assertWSError(t, guarded, ws.ErrorCodeNotFound)
|
||||
}
|
||||
|
||||
// TestAuthorizeAutomationRequest_NonAutomationPrincipalUnaffected proves the
|
||||
// guard only applies to the automation surface: a kanban-task principal (or
|
||||
// no principal at all) passes through untouched, on the early-return path
|
||||
// that never reaches automationSelfDeniedActions.
|
||||
func TestAuthorizeAutomationRequest_NonAutomationPrincipalUnaffected(t *testing.T) {
|
||||
h := &Handlers{}
|
||||
msg := makeWSMessage(t, ws.ActionMCPArchiveTask, map[string]interface{}{
|
||||
"task_id": "some-task",
|
||||
})
|
||||
|
||||
t.Run("no principal", func(t *testing.T) {
|
||||
guarded, replacement, err := h.authorizeAutomationRequest(context.Background(), msg)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, guarded)
|
||||
require.Same(t, msg, replacement)
|
||||
})
|
||||
|
||||
t.Run("kanban-task principal", func(t *testing.T) {
|
||||
ctx := mcpscope.WithPrincipal(context.Background(), mcpscope.Principal{
|
||||
WorkspaceID: "ws-1",
|
||||
CallerTaskID: "kanban-task",
|
||||
CallerSessionID: "kanban-session",
|
||||
Surface: mcpprofile.SurfaceKanbanTask,
|
||||
})
|
||||
guarded, replacement, err := h.authorizeAutomationRequest(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, guarded)
|
||||
require.Same(t, msg, replacement)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-2026-08-22: Let Users Configure Continuity, Not MCP Authority
|
||||
|
||||
**Status:** accepted
|
||||
**Status:** accepted (amended by ADR-2026-09-02-automation-self-archive)
|
||||
**Date:** 2026-08-22
|
||||
**Area:** backend, agentctl, frontend, protocol, security, workflow
|
||||
**Related ADRs:** [MCP tool profiles](2026-08-08-mcp-tool-profiles.md), [task-owned worktree lifetime](2026-08-08-task-owned-worktree-lifetime.md), [live agent permission authority](2026-08-11-live-agent-permission-authority.md)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# ADR-2026-09-02: Permit Automation Self-Archive as Terminal Completion
|
||||
|
||||
**Status:** accepted
|
||||
**Date:** 2026-09-02
|
||||
**Area:** backend, agentctl, protocol, security, workflow
|
||||
**Related ADRs:** [Let Users Configure Continuity, Not MCP Authority](2026-08-22-user-configured-automation-continuity.md), [Bind Automation Mutations to Event Targets](2026-08-09-bind-automation-mutations-to-event-targets.md)
|
||||
**Related specs:** [Automation Continuity](../specs/office/requirements/automation-continuity.md), [Automation Target Modes](../specs/office/system-design/automation-target-modes.md)
|
||||
|
||||
## Context
|
||||
|
||||
The fixed `SurfaceAutomation` MCP profile must stop an automation from using
|
||||
coordinator authority to mutate its own task, session, or blockers. The
|
||||
automation still needs one terminal operation: a hidden run task must be able
|
||||
to archive itself when its work is complete. The previous continuity decision
|
||||
described every self-target mutation as invalid, which made the watchdog
|
||||
completion path contradict the security contract.
|
||||
|
||||
## Decision
|
||||
|
||||
Allow `archive_task_kandev` to target the calling hidden automation task as its
|
||||
terminal completion signal. The request still passes the normal workspace and
|
||||
caller authorization checks and uses the normal task archive lifecycle.
|
||||
|
||||
Keep every other self-target restriction unchanged. Messaging, task updates,
|
||||
moving, dependency changes, stopping, session spawning, and question or
|
||||
permission discovery and resolution remain denied for the automation's own
|
||||
task and its sessions. Foreign-workspace targets remain denied.
|
||||
|
||||
For a `github_pr_merged` run, the event binding remains authoritative. The
|
||||
requested task must equal the persisted `automation_target_task_id`, so the
|
||||
run task cannot use the self-archive exception to archive itself instead of the
|
||||
merged pull request's target.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Scheduled hidden automations can finish by archiving their own run task.
|
||||
- The coordinator surface does not gain general self-mutation authority.
|
||||
- The merged-pull-request flow retains an exact event-target security boundary.
|
||||
- The archive handler and authorization tests must keep both the allowed
|
||||
scheduled self-archive and the rejected bound-run self-archive covered.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Keep all self-archive requests denied.** Rejected because a watchdog run
|
||||
cannot close its own hidden task through the existing agent action path.
|
||||
2. **Add a native completion action outside MCP.** Rejected because it creates
|
||||
a second completion path and bypasses the existing archive lifecycle.
|
||||
3. **Allow every self-target mutation.** Rejected because it would let a
|
||||
coordinator alter its own conversation, sessions, or blockers and would
|
||||
weaken the fixed-surface boundary.
|
||||
@@ -195,7 +195,7 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 2026-08-20-acp-client-non-underscore-extension-methods | [Route Non-Underscore Inbound Client Methods to the Extension Handler](2026-08-20-acp-client-non-underscore-extension-methods.md) | accepted | backend, protocol | 2026-08-20 |
|
||||
| 2026-08-22-persistent-fork-approval-labels | [Persist Fork Approval Labels Across Pushes](2026-08-22-persistent-fork-approval-labels.md) | accepted (amended by 2026-08-24-unified-fork-approval-label) | infra, workflow, security | 2026-08-22 |
|
||||
| 2026-08-22-plugin-version-retention | [Keep Exactly One Superseded Plugin Version](2026-08-22-plugin-version-retention.md) | accepted | backend | 2026-08-22 |
|
||||
| 2026-08-22-user-configured-automation-continuity | [Let Users Configure Continuity, Not MCP Authority](2026-08-22-user-configured-automation-continuity.md) | accepted | backend, agentctl, frontend, protocol, security, workflow | 2026-08-22 |
|
||||
| 2026-08-22-user-configured-automation-continuity | [Let Users Configure Continuity, Not MCP Authority](2026-08-22-user-configured-automation-continuity.md) | accepted (amended by 2026-09-02-automation-self-archive) | backend, agentctl, frontend, protocol, security, workflow | 2026-08-22 |
|
||||
| 2026-08-23-automation-target-modes | [Give Automations Explicit Hidden and Visible Task Targets](2026-08-23-automation-target-modes.md) | accepted | backend, frontend, protocol, workflow | 2026-08-23 |
|
||||
| 2026-08-22-preserve-newest-bounded-backend-logs | [Preserve Newest Bounded Backend Logs](2026-08-22-preserve-newest-bounded-backend-logs.md) | accepted | backend, infra, operations | 2026-08-22 |
|
||||
| 2026-08-12-plugin-webhook-auth-gate | [Require Auth for Plugin Webhooks Unless the Manifest Declares Them Public](2026-08-12-plugin-webhook-auth-gate.md) | accepted | backend, frontend, security | 2026-08-12 |
|
||||
@@ -230,3 +230,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 2026-08-31-agent-aware-mcp-tool-names | [Preserve Canonical MCP Tool Names Through Agent Namespacing](2026-08-31-agent-aware-mcp-tool-names.md) | accepted | backend, agentctl, protocol | 2026-08-31 |
|
||||
| 2026-09-01-server-owned-saved-prompt-expansion | [Keep Saved-Prompt Expansion Server-Owned](2026-09-01-server-owned-saved-prompt-expansion.md) | accepted | backend, frontend, protocol, security | 2026-09-01 |
|
||||
| 2026-09-01-passthrough-initial-prompt-turn-boundary | [Keep Passthrough Initial Prompt State in Lifecycle](2026-09-01-passthrough-initial-prompt-turn-boundary.md) | accepted | backend, workflow | 2026-09-01 |
|
||||
| 2026-09-02-automation-self-archive | [Permit Automation Self-Archive as Terminal Completion](2026-09-02-automation-self-archive.md) | accepted | backend, agentctl, protocol, security, workflow | 2026-09-02 |
|
||||
|
||||
@@ -200,7 +200,7 @@ Native session continuation and provider-managed compaction remain authoritative
|
||||
|
||||
Automation sessions receive one fixed, workspace-scoped coordinator MCP surface. The server resolves the trusted automation principal before dispatch and uses that principal for workspace, caller task, caller session, surface, and audit identity; a prompt or tool argument cannot forge those values. The catalog includes coordination and pending-question or permission actions needed by an automation, but excludes task deletion, configuration mutation, task-local authoring, provider PR/MR actions, diagnostics, plugins, and arbitrary capability settings.
|
||||
|
||||
The automation's own hidden task and every session on it are invalid targets for mutation, messaging, stopping, spawning, and blocker discovery or resolution. Foreign-workspace targets return the same not-found result as unknown targets. A task spawned on another allowed task receives that target task's normal MCP profile and never inherits the automation surface. Reused worktrees are not reset or rebased by coordinator actions.
|
||||
The automation's own hidden task and every session on it are invalid targets for messaging, stopping, spawning, and blocker discovery or resolution, and for every mutation except archiving. An automation can archive its own hidden task; that is its normal end-of-run completion signal, not a self-mutation. Foreign-workspace targets return the same not-found result as unknown targets. The archive exemption covers only the automation's own hidden task; sessions on it remain invalid targets. A task spawned on another allowed task receives that target task's normal MCP profile and never inherits the automation surface. Reused worktrees are not reset or rebased by coordinator actions.
|
||||
|
||||
## Export automations
|
||||
|
||||
|
||||
@@ -129,10 +129,15 @@ while preventing owner-wide access, self-targeting, and task-local questions.
|
||||
the automation, workspace, caller task, caller session, and surface
|
||||
identities. Handlers shall use it as the workspace boundary and audit the
|
||||
source as `automation_mcp`.
|
||||
- **AC-OFFICE-AUTOMATION-CONTINUITY-004.4:** Mutations, messages, stopping,
|
||||
spawning, and blocker discovery or resolution through `SurfaceAutomation`
|
||||
shall reject the automation's own hidden task and every session on it, as
|
||||
well as foreign workspaces.
|
||||
- **AC-OFFICE-AUTOMATION-CONTINUITY-004.4:** Messages, stopping, spawning, and
|
||||
blocker discovery or resolution through `SurfaceAutomation` shall reject the
|
||||
automation's own hidden task and every session on it, as well as foreign
|
||||
workspaces. Archiving shall be exempt from the self-target rejection so
|
||||
that an automation can archive its own hidden task as its end-of-run
|
||||
completion signal; a run bound to a specific target task shall still
|
||||
archive only that bound target. Every other mutation on the automation's
|
||||
own hidden task shall remain rejected, as shall any foreign-workspace
|
||||
target.
|
||||
- **AC-OFFICE-AUTOMATION-CONTINUITY-004.5:** A task spawned on another task
|
||||
shall receive that target task's normal profile and shall not inherit
|
||||
`SurfaceAutomation` from its caller.
|
||||
|
||||
@@ -26,7 +26,8 @@ This design preserves the technical source detail for `REQ-OFFICE-AUTOMATION-RUN
|
||||
## Migrated source detail
|
||||
|
||||
Decision:
|
||||
[ADR-2026-08-22-user-configured-automation-continuity](../../decisions/2026-08-22-user-configured-automation-continuity.md)
|
||||
[ADR-2026-08-22-user-configured-automation-continuity](../../../decisions/2026-08-22-user-configured-automation-continuity.md)
|
||||
and [ADR-2026-09-02-automation-self-archive](../../../decisions/2026-09-02-automation-self-archive.md)
|
||||
|
||||
## Why
|
||||
|
||||
|
||||
@@ -187,7 +187,9 @@ The task mode is server-owned configuration, not an MCP argument. Hidden
|
||||
automation identity is derived only from a task whose origin and metadata bind
|
||||
it to the automation. Visible automation tasks are not accepted as hidden
|
||||
coordinator callers. Workspace authorization and exact run identity remain
|
||||
required for all automation mutations.
|
||||
required for all automation mutations. The one self-target exception is
|
||||
`archive_task_kandev` on the calling hidden run task as terminal completion;
|
||||
all other self-target mutations and foreign-workspace targets remain denied.
|
||||
|
||||
## Observability
|
||||
|
||||
@@ -201,4 +203,5 @@ visible task state.
|
||||
## Related decisions
|
||||
|
||||
- [User-configured automation continuity](../../../decisions/2026-08-22-user-configured-automation-continuity.md)
|
||||
- [Automation self-archive](../../../decisions/2026-09-02-automation-self-archive.md)
|
||||
- [Automation target modes](../../../decisions/2026-08-23-automation-target-modes.md)
|
||||
|
||||
Reference in New Issue
Block a user