Notify Tentacles when pending Remote approval is revoked
This commit is contained in:
1 parent
205cb27483
commit
9bc74dbb13
8 files changed
+61
-12
No files matched your search
@@ -154,9 +154,11 @@ Its sequence starts at one and increases exactly once per event. Supported kinds
|
||||
- `remote.action`: sequence, kind, action token, host-issued epoch, role and approved
|
||||
grant subset. Created only by the internal core authorizer. Approval expires
|
||||
after 30 seconds if not consumed. No network secret is included.
|
||||
- `remote.revoked`: sequence, kind, lease, epoch and reason (`closed`, `expired`,
|
||||
- `remote.revoked`: sequence, kind, epoch, exactly one of action or lease, and reason (`closed`, `expired`,
|
||||
`local`, `replaced`). Failure to enqueue revocation retires the session; it
|
||||
cannot block immediate cancellation. An unconsumed approval expires silently.
|
||||
cannot block immediate cancellation. Pending approval expiry/revocation carries
|
||||
its action token; an opened lease carries its lease token. Both retire external
|
||||
setup workers through the same epoch-bound notification.
|
||||
|
||||
Child calls:
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
|
||||
defer stopCommand()
|
||||
stop := context.AfterFunc(command.session.OutputContext(), cancel)
|
||||
defer stop()
|
||||
// Policy cancellation can interrupt setup before a lease exists, when no
|
||||
// remote.revoked event is emitted. Tell the original continuation to stop its
|
||||
// Policy cancellation can interrupt setup before approval exists.
|
||||
// Tell the original continuation to stop its
|
||||
// inspection/setup workers before releasing the interaction slot. Never pass
|
||||
// cancelled form values or reuse the cancelled interaction context.
|
||||
cleanupNext := ""
|
||||
|
||||
@@ -182,8 +182,8 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
|
||||
access.timer.Stop()
|
||||
}
|
||||
session.release_remote_owner()
|
||||
if notify && access.lease != "" {
|
||||
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Lease: access.lease, Epoch: access.epoch, Reason: reason}
|
||||
if notify {
|
||||
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Action: access.action, Lease: access.lease, Epoch: access.epoch, Reason: reason}
|
||||
if !session.remote_event_locked(&event) {
|
||||
session.fail_locked(ErrLimit)
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ func TestRemotePolicyRevokesPendingAndLiveSDKAccess(t *testing.T) {
|
||||
if _, code := remoteCall(t, session, "remote.heartbeat", tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}); code != -32001 {
|
||||
t.Fatal("heartbeat revived lease", code)
|
||||
}
|
||||
var event tentaclewire.SessionEvent
|
||||
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Lease != lease.Lease {
|
||||
t.Fatal("revocation event", event)
|
||||
}
|
||||
}
|
||||
var event tentaclewire.SessionEvent
|
||||
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Epoch != action.Epoch || event.Lease != lease.Lease || (!opened && event.Action != action.Action) {
|
||||
t.Fatal("revocation event", event)
|
||||
}
|
||||
}
|
||||
// A fresh approval can replace the cancelled epoch even if its
|
||||
|
||||
@@ -470,3 +470,33 @@ func TestSessionAggregateQueueBound(t *testing.T) {
|
||||
t.Fatal("aggregate byte ceiling exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingRemoteRevocationReachesBothSDKs(t *testing.T) {
|
||||
for _, language := range []string{"go", "python"} {
|
||||
for _, reason := range []string{"local", "expired", "replaced"} {
|
||||
t.Run(language+"/"+reason, func(t *testing.T) {
|
||||
session := startSessionSDK(t, test_manager(t, DefaultLimits(), nil), language, "pending-revoke")
|
||||
action, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invokeSessionSDK(t, session, "event", nil)
|
||||
session.mutex.Lock()
|
||||
if reason == "expired" {
|
||||
session.refresh_remote_locked(time.Millisecond)
|
||||
} else {
|
||||
session.revoke_remote_locked(reason, true)
|
||||
}
|
||||
session.mutex.Unlock()
|
||||
var event tentaclewire.SessionEvent
|
||||
raw := invokeSessionSDK(t, session, "event", nil)
|
||||
if json.Unmarshal(raw, &event) != nil || event.Kind != "remote.revoked" || event.Action != action.Action || event.Lease != "" || event.Epoch != action.Epoch || event.Reason != reason {
|
||||
t.Fatal("pending action revocation was not delivered", string(raw))
|
||||
}
|
||||
if _, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: action.Action, Epoch: action.Epoch, Role: action.Role, Grants: action.Grants}); code != -32001 {
|
||||
t.Fatal("retired action opened a lease", code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func ValidSessionEvent(event SessionEvent) bool {
|
||||
case "remote.action":
|
||||
return event.Speech == nil && len(event.Action) == 32 && event.Lease == "" && event.Reason == "" && ValidRoleGrants(event.Role, event.Grants)
|
||||
case "remote.revoked":
|
||||
return event.Speech == nil && event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
|
||||
return event.Speech == nil && ((event.Action == "" && len(event.Lease) == 32) || (len(event.Action) == 32 && event.Lease == "")) && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
|
||||
case "remote.output":
|
||||
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech != nil && ValidRemoteSpeech(*event.Speech)
|
||||
}
|
||||
|
||||
@@ -33,3 +33,18 @@ func TestSessionNegotiationRequiresIndependentGrants(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteRevocationIdentifiesExactlyOneActionOrLease(t *testing.T) {
|
||||
token := "0123456789abcdef0123456789abcdef"
|
||||
for _, item := range []struct {
|
||||
action, lease string
|
||||
valid bool
|
||||
}{
|
||||
{token, "", true}, {"", token, true}, {token, token, false}, {"", "", false}, {"short", "", false},
|
||||
} {
|
||||
event := SessionEvent{Sequence: 1, Epoch: 1, Kind: "remote.revoked", Reason: "expired", Action: item.action, Lease: item.lease}
|
||||
if ValidSessionEvent(event) != item.valid {
|
||||
t.Fatalf("unexpected validation for %+v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,9 @@ class SessionClient:
|
||||
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
|
||||
raise ProtocolError("Invalid action event")
|
||||
elif event["kind"] == "remote.revoked":
|
||||
if ("speech" in event or not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
|
||||
identity = ((_token(event.get("lease")) and event.get("action", "") == "")
|
||||
or (_token(event.get("action")) and event.get("lease", "") == ""))
|
||||
if ("speech" in event or not identity or event.get("role", "") != ""
|
||||
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
|
||||
raise ProtocolError("Invalid revoke event")
|
||||
elif event["kind"] == "remote.output":
|
||||
|
||||
Reference in new issue
Block a user