Replies: 3 comments
|
Very interesting. Just kindly share some related stuff, we have already got endpoint to deliver result back to a session. Recently some new tools are added to manage top level session (chat slot) in the mcp-core. |
|
FYI @CrysisDeu as we think about the app development lifecycle |
|
Thanks @iamwhatever; you're right, and it changed what I'm asking for. Let me be specific about what I found, because the gap turned out to be narrower and sharper than what I originally wrote.
So the revised ask is one flag, not a new subsystem: let a caller opt in to persisting the hook session (write it to history and surface it as a slot), instead of destroying it after the turn. Concretely, what I'd want from
Defaults I'd propose, so this doesn't widen anyone's blast radius: off by default (unchanged behaviour for every current caller), gated by the same webhook token, and subject to the same concurrency cap. If a persisted session should count against a different limit than an ephemeral one, that seems right to me. I'll note the direction already exists elsewhere: On the second ask (routing an event to a crew), which I don't think the endpoint supersedes. Reaching a crew through the webhook token works, but only because the operator pasted a token into an app's config: it's a workaround that happens to function, not a sanctioned capability, and it launders the app's identity through a credential that carries no scope. What I asked for was an operator-granted, consent-gated app→crew route with the grant stored in framework config rather than in app-writable state, so the operator can see and revoke it. That still seems worth having independently of anything above, and I'd rather it be a real permission than an accident of token possession. Happy to implement either piece if the direction is acceptable; the persist flag looks like a small change to |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Discussions → Ideas. Floating this as a design idea before writing any code — I'd rather
hear "not in this shape" now than after a PR. If it's "no in principle", that's a useful
answer too.
Where this comes from
I'm building a third-party app that receives webhooks (from Github or ANY other source), buffers them in SQS,
and hands each event to an agent. Building it, I realised the thing I'd written is a channel in all but
name: an inbound transport that turns outside events into agent work. Same shape as Slack,
Discord, Telegram, or cron — just speaking HTTPS-plus-a-queue instead.
And that's where I hit a wall that I don't think is about my app. Every part of Kiro Crew that
has this shape gets to own its sessions, so its work shows up on the board and can be opened and
replied to. A third-party app can't. The dividing line isn't what the code does — it's
where the code lives:
taskrunner.py, held asstate.task_runner).It holds the
SessionManagerdirectly, mints its own keys (taskrunner:run:…), and picks anyagent.
app.jsonis the only file in its directory — a UIpage and a permission. The engine is core (
channel.py,channels.py,channel_history.py).cron:{job.id}sessions into visible slots from core(
dashboard/cron_inject.py).So the capability is real, mature, and used by three producers — but it's reachable only by
being core. My app does the same job as those, and the only difference is that it's installed
rather than shipped. That asymmetry is what I'd like to close.
To be concrete about the gap, here's what a user gets from my app today versus what I'd like
them to get:
The first three are one capability, not three. The fourth is a different question with real
security weight, so I've split it out rather than bundling them.
It isn't just my app
Anything that turns outside events into agent work has this shape: a webhook ingress, a queue
consumer, an IMAP or SMS bridge, a CI listener, a bridge for any transport Kiro Crew doesn't
natively speak. For all of them an event is not a sub-task of a chat turn — it's its own
conversation someone may later want to read, resume, or steer. Today each of those authors gets
the subagent primitive, which is the wrong one: invisible, short-lived, unrepliable.
There's a "do nothing" cost worth naming too. Without a sanctioned path, an app author who wants
this either does without, or reaches for something worse — and the worse options are the
concerning ones, because they'd each re-derive session ownership logic that has already produced
real bugs (see the failure history below).
The first thing I want: apps own the sessions they deliver into
I'm volunteering to implement this, not asking someone else to. Concretely:
One SDK call,
ctx.sessions.deliver(key, prompt, title), gated by a new deny-by-defaultpermissions.sessions. The framework owns key derivation (app:<app_name>:<suffix>, suffixsanitised — an app can never name
slack:…,dashboard:…, or another app's keys), theownership/idempotency check, and surface publication. Implementation mirrors
cron_inject.pyrather than introducing a parallel path.
Defaults I'd pick unless you'd rather they were different:
creating a second one. At-least-once transports make this mandatory, and it's the Slack inbound message handling can create a duplicate/competing session for a thread already linked to a dashboard session #2796
failure mode.
ephemeral machinery rather than adding a parallel notion of either.
log_api_access, like other app-initiated privileged calls.The one decision I need from you: is this something you'd accept in principle? If yes I'll
open a PR with the app above as the working consumer, so you can see the capability exercised
end-to-end rather than in the abstract. If you'd prefer a different shape — an event the
framework consumes, or extending
channelsto accept app-registered transports — say which andI'll build that instead. If it's a no, I'll stop here and keep the fallback.
One thing I want to be explicit about rather than gloss: linked sessions have a real bug history
(#2796 split one logical conversation in two, #2462 cancelled the wrong session, #2795 is still
open). I read that as the argument for a single framework-owned entry point rather than
against the idea — the failures came from a second inbound path reasoning about ownership
differently from the first, and the worst outcome would be N app authors each hand-rolling their
own version. Details in the failure-history section below. If your conclusion is "we don't want
more producers on that mechanism at all", that's a legitimate reason to decline.
The second thing I want: routing an event to a crew
I'll state this plainly rather than bury it, because it's the other half of what my app needs.
When a webhook arrives I want to route it to a crew the user chose — their triage crew, their
deploy-watcher crew — not only to an agent my app happens to ship. Today
apps/spawn_sdk.pyrestricts an app to its own
<app>--*agents, so a crew is unreachable.I am not asking you to relax that check. I'm asking whether an operator could grant an app
access to specific crews: declared as intent in the manifest, granted per-crew by the operator,
enforced by the framework. Deny-by-default, revocable, visible.
The check is doing real work and I don't want it weakened for every app at once. A user crew's
agent carries none of the
managedToolPolicyexclusions that app agents carry, it brings aworkspace(filesystem scope the app was never granted) and a memory store it can write to, andfor an ingress app the prompt is built from an inbound payload — so "route to any crew" would put
attacker-influenced text into a fully-capable agent with real files and real memory. Hence
consent rather than a wider default. Full reasoning in the security section below.
One constraint makes that consent real: the grant must live in framework-owned config, not in
app-writable state. My app's routing table lives in its own
data_dirand my app writes it, soit cannot be the consent record. The grant belongs alongside app enablement, and my crew picker
should only be able to offer crews already granted.
This ask is independent: a no here doesn't affect the session-namespace one.
The sections below are supporting detail — what exists today, the failure history, the API
sketch, and the security analysis behind the crew grant.
How the platform already does this (code references)
Non-dashboard sessions being visible and browsable is established behaviour:
dashboard/state.py:1202—self.linked_session_key: str = "" # when set, _run_chat uses this as session keydashboard/state.py:3296—linked_session_keyis a named kwarg ofget_or_create_slot,documented as binding "a new slot to the session its conversation" runs on
dashboard/chat_utils.py:519-535—effective_session_key(): "A channel-born slot carriesthe real channel key (
slack:<ts>) inlinked_session_key, so its turns run on thechannel's own session and its transcript IS the channel transcript"
history.py:904-906—_safe_key()is a genericre.sub(r"[^\w\-.]", "_", key), so anyprefix:idfolds to a filename with no per-transport logicdashboard/chat_persistence.py:573,922,1649—linked_session_keyis persisted in slotmeta, so such a tab survives a restart
Already used by three producers, only one of which is a chat transport:
channel:…/slack:…dashboard/channel_slots.py:267cron:{job.id}dashboard/cron_inject.py:90dashboard/workflow_inject.py:156The only reason an app can't do this is that slot creation touches
DashboardState, which theapp sandbox rightly cannot reach.
Why it must be ONE framework-owned entry point (and the bug history)
I'd rather name the risk than sell the mechanism as free. Linked sessions have a real bug
history, all in the same area:
thread already linked to a dashboard session, "effectively splitting one logical conversation
into two". Root cause: a newer inbound path "checks only an in-memory, explicit link to
dashboard mechanism", and a false result there does not imply no persisted owner exists.
cancelled nothing.
accurate self-description of that session.
Those failures came from a second inbound path reasoning about ownership slightly differently
from the first. So the ask is explicitly not "expose
linked_session_keyto apps" — it'sone framework-owned function that owns key derivation, ownership/idempotency checks, and
surface publication, so every app-delivered session inherits the fixes those issues produced
instead of becoming a fourth divergent implementation.
If the conclusion is "we don't want more producers on that mechanism at all", that's a
legitimate reason to decline, and worth saying plainly.
Two invariants I'd want that entry point to own, both learned from the above:
the same event twice; the same logical event must resolve to the same session rather than
splitting it — the Slack inbound message handling can create a duplicate/competing session for a thread already linked to a dashboard session #2796 failure mode.
cron_inject.pydocuments it: publish the slot to thesurface registry before anything routes against it, or "a created-but-unpublished slot
silently fails those gates until some unrelated slot change happens to republish".
API sketch
One SDK method, opt-in per app, deny-by-default:
Constraints I'd suggest baking in:
app:<app_name>:<suffix>andsanitises the suffix. An app cannot mint
slack:…,dashboard:…, or another app's keys.spawn/storagetoday.cron_inject.pyand follows it.Security analysis behind the crew grant (why consent, not a wider default)
Dispatching to a non-app agent would today:
managedToolPolicyis framework-ownedand force-refreshed on every install precisely so app agents "cannot reach
spawn_run/cron_*/task_runor inherit the global mcp.json" (apps/bridges.py:668). Auser crew's agent carries none of those exclusions, so an app naming one reaches those
capabilities indirectly — including spawning further agents.
workspace; dispatching to itmeans prompt-driven work in that project directory. No app permission covers filesystem
scope beyond the app's own
data_dir.to them. Pollution is the subtle half: false facts written there silently influence the
human's later sessions in that workspace.
built from an inbound payload, so the real shape is "internet-reachable input becomes the
prompt of a fully-capable agent with a real workspace and memory". Bearer auth
authenticates the sender, not the payload.
None of that argues the operator shouldn't be able to do this — they can already wire a webhook
to any crew with their own script. It argues the authority must be explicitly granted rather
than inherited from having installed the app, because an app is a distributable artifact
someone else installs.
Two further limits mean "just ship an app-owned agent" isn't a full substitute: app agents are
absent from
config.agents, soresolve_agent_bindings(config/loader.py:6256-6274) bindsthem to the default workspace and memory store — a crew mirror reproduces the model but not
the memory — and their tool policy is narrowed as in (1).
Alternatives considered, and today's permission surface
SpawnSDKprefix check — rejected: removes a check that protects every app, tofix a problem that's really about the wrong primitive.
parent chat; the parent is the app.
what an app platform should avoid.
job history. Mine ships this way today, with the limitation stated in its UI.
For reference, the complete set of capabilities an app can be granted today is
api(exact-path allowlist),
events,storage,network,cron,spawn, plus declaredmcpservers and system commands (
apps/manifest.py:411-418). There's no session, agent, workspace,or dashboard capability of any kind — which is why both asks introduce new consent-gated
permissions rather than relaxing existing ones.
All reactions