Closed expede closed 1 year ago
Hmm invocation and pipelining don't have to be in the same spec. It could be broken down really granularly:
UCAN -> Invocation -> Receipt -> Pipeline
⌞_____________________⌟
These two probably
make most sense
together
@pdxjohnny GitHub snitched on you above :wink:
Here another work-in-progress that may be relevant:
You've likely seen it already, but just in case not :)
@pdxjohnny GitHub snitched on you above 😉
Here another work-in-progress that may be relevant:
You've likely seen it already, but just in case not :)
/me screams with excitement 😝👍👍 oh boy oh boy do I have some reading to do. I love what I’m seeing I hadn’t seen this and I will be digging!
😆 I'm having to break certain features across IPVM and UCAN. Organization is hard!
Hello reviewers 👋 Irakli has put feedback into the doc thus far, but at time of posting hasn't seen big chunks of the text (which I've written in mostly one sitting today). If anything (or everything) in here is nonsense, that's my fault and not his! (He didn't ask me to write this, but I figured his name is on the thing so I should at least flag that he hasn't necessarily endorsed this version)
@expede drawing analogy between capabilities and references vs invocation is really helpful I think. Also made me realize that conceptually I had been thinking of capabilities similar how variables in Elm are functions without arguments which does blur those lines a bit.
Reflecting bit more on the spec I’ve also recognized the fact that some of our capabilities don’t have providers because they aren’t really invocable, instead they serve role of a parameter for another capability invocation.
All that to say that I’m warming up to the idea that they should be distinct.
@expede I am wondering what is a motivation for an envelope as opposed to say extension of the UCAN model.
It seems to me that it should be possible to add field along the lines of run
(which ai’d personally call invoke) to the UCAN to communicate both intent and scheduling info needed. If nothing else it would remove need for another signature (& verification) and it seems like it wold be reasonable to use invocation as proof as well since it essentially is with bit more details
Also migrating our current system to a one that simply adds invoke: “*”
to UCANs would be really straightforward
@Gozala
for an envelope as opposed to say extension of the UCAN model
There's really two:
I initially want back and forth a bit on the right way, but supporting promise pipelining made it really clear for me.
With pipelining, the output of a prior action attenuates a later one. But you don't know what the result is going to be: you have to give the executor the authority to run with possible output of the earlier action on the second one. There's no way around this, because the executor could just lie about the output of the first action (they're in control).
Having an envelope means that we make really clear "you're delegating this amount of authority, but asking them to perform these actions with that authority." It also prevents unresolved promises from being passed around endlessly in UCAN chains.
The proposed design from this PR keeps the delegation & authority layer really focused on that, and gives us lots of flexibility to add features that don't make sense in a delegation chain.
Another design is to add a field like invoke: true
to the UCAN. Now that information can live "forever" when further delegated. This is meaningless information in a chain:
Alice --> Bob --> Carol ==[invoke all]==> Dan ==[invoke email]==> Erin
(Dan sub-invokes email as part of the job for Carol)
If the UCAN has time bounds of a year, they "should" only invoke it once. There's no way to enforce/track this without external state.
Erin also doesn't need to care that Carol -> Dan
was an invocation. All that matters to Erin is that she has been passed the relevant authority, and that the Dan->Erin
request includes the "and please run it". With an envelope, we have something like this:
--> is delegation
==> is invocation
Time runs left-to-right
Alice --> Bob --> Carol ----------------> Dan ------------------------------> Erin
Carol ==[invoke all]==> Dan
Dan =========[invoke email]=======> Erin
[later] Dan ==[pipeline dns]==> Erin
In this version, the delegation chain is as normal. It's clear that Alice doesn't expect Bob to run the invocations, but Carol does signal to Dan. Dan removes the invocation wrapper from Carol -> Dan
, and Dan can subdelegate as needed. He can also exercise his authority to delegate to Fred (not on the diagram) without asking for an invocation, because Fred may also need the ability to send some email but not part of Carol's job.
The other difference is that invocation itself doesn't have time bounds. It's a request to do something "now", and exactly once (unless e.g. scheduled in the task description... if you send a message to cron
you're expecting scheduling). UCANs have time bounds to limit how long something can be done, and it's a hard limit because of the principle of least authority. Invocation is a soft ask to "please run this thing once", but we have no guarantees that it will be once or 1000x. The meaning is different between the two.
...I'm going to put that diagram in the spec after I run a few errands, I think it's helpful for a few reasons
Ah. I actually think that invocations need named actions beyond their resource. If I need to run the same action 3 times with the value of the prior step, then they need to be disambiguated somehow. The array inside ability doesn't give this to us, because it's multiple configuration objects on a single action, not multiple actions.
A totally contrived example is the invoker wants to record how long the round-trip is to the executor. They need to record the execution time to a datastore several times. The actual capability is something like:
{
"somedb://example.com": {
"crud/update": [
{"time": "$NOW"},
{"user": "admin"}
]
},
"https://example.com/logger": {
"crud/update": [
{"counter": "+1"}
{"_": {"ucan/promise": ["somedb://example.com", "crud/update"]}}
]
},
"somedb://example.com": {
"crud/update": [
{"time": "$NOW"},
{"user": "admin"},
{"_": {"ucan/promise": ["https://example.com/logger", "crud/update", hash(promise_above)]}}
]
},
"https://example.com/logger": {
"crud/update": [
{"counter": "+1"}
{"_": {"ucan/promise": ["somedb://example.com", "crud/update", hash(promise_above)]}}
]
}
}
Because the keys are now flat at the top level in UCAN, we can't express duplciate actions.
However, if we name them, we both get better readability, disambiguate, and self-document. The tradeoff is that the layout is different from core UCAN, but I can live with that:
{
"step-zero": {
"somedb://example.com": {
"crud/update": [
{"time": "$NOW"},
{"user": "admin"}
]
}
}
"step-one": {
"https://example.com/logger": { // first one
"crud/update": [
{"counter": "+1"}
{"_": {"ucan/promise": ["/", "step-zero"]}}
]
}
"step-two": {
"somedb://example.com": {
"crud/update": [
{"time": "$NOW"},
{"user": "admin"},
{"_": {"ucan/promise": ["/", "step-one"]}}
]
}
},
"and-the-end": {
"https://example.com/logger": {
"crud/update": [
{"counter": "+1"}
{"_": {"ucan/promise": ["/", "step-two"]}}
]
}
}
}
A lot of this is driven by use cases in IPVM, but there's really nothing special to IPVM about it.
☝️ The other option for the above is to use the UCAN 0.9 syntax, which also solves this problem. It's less readable to reference actions inside an invocation, though.
☝️ (and of course, you can still use "*"
in the simple case and just skip all of this detail is you're e.g. not using pipelines)
There's really two:
- Pipelining
@expede I don’t think pipelining would in any way be affected whether you have you have inv link from Invocation to UCAN or other way round link from UCAN to Invocation. It mostly affects the layout of the data and avoids second signature.
It is true that you’ll end up carrying around invocation details if you wish to use it as a delegation, unless there are some privacy issues with it, I don’t really see it as problem. I imagine that delegation chains will simply ignore that field as it’s not relevant in that context.
In terms of flexibility, again I don’t think it changes much, you still have an optional “invoke” field with conforming to “Invocation” schema defined here, which can be extended in any way desired without really affecting UCANs. From UCAN schema perspective invoke field could be treated as Any.
- Separation of concerns
Ok perhaps. It is true that info about invocation is now tied to UCAN forever, yet I’m not sure why is that problem. UCANs already allow sticking bunch of other things and will carry them around, but so far this flexibility proved useful not harmful.
Having another field with some details on how agent wanted it be executed does not seem like problem to me, unless of course there is a reason to conceal it.
——
I feel like I have to provide some justification for why even consider pushing invocation info into a UCAN field as opposed to enveloping it.
I already get ppl complaining about all the complexity
I'd love to know in which ways so that we can reduce complexity in the core spec! I hear this too, but it's usually lack of familiarity with capabilities (over ACLs) and trying to work with completely stateless patterns more than actual base-bottom inherent complexity.
This spec, by adding features at all, increases the complexity. I'm not aware of any way around adding these features while reducing complexity. Arguably, adding all of this on to the core UCAN spec would make all of their use cases more complex because they have to be aware of invocations, promises, and so on.
FWIW as soon as this spec gets merged, Fission is going to start implementing it. It can be done on top of v0.9 today with no changes. It can be shipped as a separate library, even.
With the separation proposed in this spec, someone who just wants to use UCAN for capabilities and has an implicit invocation (e.g. RESTful API) can just ignore the invocations, named actions, pipelining, receipts, etc and not have to deal with them in their implementation. Adding these features as something required to be spec compliant certainly isn't going make it any less complex!
Because we need to be able to reference a capability more than once for invocations (but not delegation!), putting all of this info into the same token format has consequences. You'd need one of the following:
[{with, can, nb}]
, since they can be duplicated and distinguished by indexA previous version of this proposal looked a lot more like the v0.10 spec, but I was unable to make promises work inside a single invocation. Hence the changes at the invocation layer.
One less signature to create and validate.
This is the thing that I like least the proposal's current form, too. But signatures are not difficult to work work. Mainly they're slow compared to a lot of other local operations.
I'd considered the opposite, where you put the invocation's CID in the UCAN, but this doesn't let you re-invoke without issuing another complete UCAN, which also seems wasteful. If the main concern is about the signature, we can explore this, though.
now tied to UCAN forever
It's mostly meaningless — and possibly misleading — information. Carrying unresolved promises around doesn't help anyone, and in the worst case makes it look like something will be attenuated when it's not actually.
UCANs already allow sticking bunch of other things and will carry them around, but so far this flexibility proved useful not harmful.
Indeed: as extensions! They're not expected to be understandable by everyone who uses the system. Invocations don't have that luxury because they end up throughout the token, and by the nature of signed data it's for all time. For example, because a promise can end up anywhere in an invocation, anyone using this spec needs to understand how to resolve these fields relative to another invocation's return. If someone doesn't understand that a promise doesn't actually attenuate a capability (which would be an easy mistake to make), they wouldn't be able to use the otherwise totally valid capability, because it looks like it's attenuated with extended fields.
@expede I don’t think pipelining would in any way be affected whether you have you have inv link from Invocation to UCAN or other way round link from UCAN to Invocation. It mostly affects the layout of the data and avoids second signature.
Ah! @Gozala I think I may have misunderstood you before! Based on my new understanding, to confirm: are you saying that you want to put the CID of an invocation in the UCAN, not that you want to support all of these features in UCAN directly?
If so, I'm not dead set against it! I think that there's tradeoffs both ways, and I'm open to exploring that version to avoid the extra signature.
Ah! @Gozala I think I may have misunderstood you before! Based on my new understanding, to confirm: are you saying that you want to put the CID of an invocation in the UCAN, not that you want to support all of these features in UCAN directly?
Exactly UCAN spec does not need to specify any of it, just say that it may have such field and it should not be stripped essentially.
If so, I'm not dead set against it! I think that there's tradeoffs both ways, and I'm open to exploring that version to avoid the extra signature.
Yes there are tradeoffs and weighting them against one another is what I’m after. Out of exchange so far, I think two tradeoffs had been identified with this approach
Although I think this is bit moot, because with envelope you still need to create new envelope and link to same chain. With alternative design you don’t need envelope in first invocation, but you’ll have to create one on re-invocation
Although I think this is bit moot, because with envelope you still need to create new envelope and link to same chain. With alternative design you don’t need envelope in first invocation, but you’ll have to create one on re-invocation
Right, but you always have to signal invocation to distinguish the intent. A UCAN on its own doesn't carry enough information unless you have additional information from context.
Yes there are tradeoffs and weighting them against one another is what I’m after.
Awesome 👍 Sorry if I came off a bit defensive, I misread your concerns.
unless you have additional information from context
Our options, AFAICT, are something like this:
invoke: true
flag, and add promises etc throughout core UCANs (I don't like this one)Right, but you always have to signal invocation to distinguish the intent. A UCAN on its own doesn't carry enough information unless you have additional information from context.
To be more specific, I imagine first invocation would be something like
const first = {
iss: “did:key:invoker”,
aud: “did:key:executor”,
att: […],
prf: […],
run: { … }
}
Where re-invocation would end up looking like
const second = {
iss: first.iss,
aud: first.aud,
att: first.att,
prf: [first], // in this context .run of the first is simply ignored
run: { … }
}
An invoke: true flag, and add promises etc throughout core UCANs (I don't like this one)
I don’t think this is worth considering, because no 2 in simple case would end up looking more or less like this with a only difference been that invoke: true
turns into run: “*”
or something along those lines.
Which is kind of the appeal of no 2, if you just want invoke: true
you basically have it. But if you want something more advance that is also possible just specify more complex run
To be more specific, I imagine first invocation would be something like
Thanks for getting concrete! It's helpful :) Text communication is hard!
This is roughly equivalent to 2. A nested invocation object
above. I definitely see the appeal of this for the typical case!
Where re-invocation would end up looking like
Re-invocation ends up repeating a lot of information though, no? The DIDs, the entire att
field, etc.
I guess the reasoning is that it's less common.
What if we supported both? Essentially the invocation spec would say "you need these fields, we don't care how you supply the UCAN", which is a bit like your excellent "provide any disconnected proofs as long as the principals and attenuation line up" proposal. Passing them in the same object is the intersection type of the two.
This means that if you get a UCAN with a nested run
field (ideally by CID to save bytes on all later delegations), then great! But you can also pass around a "disconnected" invocation object, but it just has to be signed with the same issuer as whichever UCAN will power its authority. We pull the UCAN field out of the standlone invocation, and you have to supply one along with the invocation the same as in the proposed change to UCAN v0.10.
It's definitely one more case to have to reason about at runtime, but I think it may just solve the tradeoffs. Thoughts?
(Perhaps for context: we expect re-invocaton to be quite common in IPVM)
Wild Idea: What if we supported both?
You know, one way we could manage this is to break up the spec. The really simple case of "here's some capabilities, and also run them" is equivalent to a flag.
There a legitimate cases where you'd want to add more configuration to a specific invocation, but it's almost always possible to do in the UCAN capabilities directly. It may make it impossible to further delegate the chain, but you can do this.
Pipelining is the big differentiating feature for why you'd want to externalize the invocation (unless you're going to call a simple UCAN several times and want to save a few bytes).
We could make a really simple invocation spec that only has run: "*"
, and pull promises, pipelines, receipts, and the option to have standalone invocations into an separate spec that depends on the simpler one. This way, you don't have to support ALL of these features if you only need the simplest case 🤔
Besides, ucan-promise
is another great spec name 😁
@matheus23 came up with another pun name for this spec: "UDO: UCAN Delegated Orders" 🤣
@matheus23
I love this, which is why I wanted to engage with it 😄
Please do! 🎉
whether we should make idempotent and commutative invocations special?
This stuff is all over in the very WIP IPVM spec — it's a very core aspect to that system :)
I wasn't able to find a common use case for this beyond dataflow. With dataflow, we're asking the user to add dependencies between segments which forces order. Everything else is commutative since there's no data dependency between the two
Re-invocation ends up repeating a lot of information though, no? The DIDs, the entire att field, etc.
I guess the reasoning is that it's less common.
Only added field there in comparison to Invocation
model specified there is att
just take make it UCAN compatible. iss
, add
and all the rest already exist in Invocation
so it's just the same.
As of att
we don't really need to repeat all the stuff in there either, we could either:
att: [{ "with": "ucan:*", "can": "*" }]
implying do everything in the prf.att
when invoke
is set, to imply all the caps in the prf
.@expede here is the summary from our conversation earlier today:
run
field from Invocation
model and adding it as optional invoke
to UCAN would have following benefits
iss
, aud
, nnc
, ext
, fields.run
field.invoke
field is still a UCAN.system/execute
that may take roughly what's in the run
field in an nb
. Going about this way enables developing alternative orchestration syntaxes to be developed.system/execute
capability. Or it could be given limited program calling capability by restricting programs it may call.@Gozala Thank for taking the time earlier to discuss the spec! I still need to read the latest on w3-machine
, but I bounced some ideas around with @QuinnWilton and @blaine late this afternoon. I think it helped clarify a few things for my own thinking. Here's my current picture:
The spec as it stands is probably not a great fit for the stuff you're doing at DAG House. In fact: it's probably not useful to many use cases. If you send a UCAN in a header to POST https://example.com/rpc
, then we can infer the same information as run: "*"
from the HTTP method. It's really only when you want to describe the direct interaction between objects in a transport agnostic way that signalling becomes useful. This then needs to live inside the data itself, but there's nothing special about this as a wrapper or not.
It's probably worth putting some info about intention signalling in general (over HTTP and other methods) in the core UCAN spec FAQ. I keep coming back to the idea that we should keep additional fields out of the core UCAN token because it makes things like ucan-ipld
much harder. We can still prototype this kind of thing by putting it in the fct
, and if it's useful people will probably do that!
Adding invocation info to the core UCAN token is a one-way door. Once it's in there, we have to account for it for a long time (as we've seen with changes to UCAN previously it's always painful).
Since IPVM has a really clear use case for promise pipelining and all that entails, we need some re-invocation signalling either way. If we later decide it's a bad idea, it's totally fine: nothing is baked in at the core UCAN layer, so we just stop using them. It's a totally separate object. If we find that it's really useful and want to avoid the extra wrapper in the simple case, we can add that later. It's much harder to go the other way around.
I keep coming back to the fact that invocation is separate from authority. Yes, there's an extra signature to check in the simple case. I'm willing to force that in the first version to get experience with it before we start optimizing.
VM could provide ability system/execute that may take roughly what's in the run field in an nb. Going about this way enables developing alternative orchestration syntaxes to be developed.
After thinking about it some more, this is actually probably not how I'd model this. It directly mixes concerns of authority with execution, which I think need to be extra clear. You totally can do this (and in fact no one can stop you, because UCANs are extensible), but special first-class with and can in UCAN have not been popular in the past (looking at you my://
and ucan/delegate
).
MAY do it dynamically as long as it has capabilities to do so. Above is similar to how solana does cross-program invocations.
Indeed, this looks a LOT like synchronous object capabilities — AFAICT a subset of CapTP — which totally makes sense in a blockchain context 💯
No more need to repeat iss, aud, nnc, ext, fields.
Minor thing: we don't repeat the iss
and aud
(or even the ext
) fields in the invocation spec. It uses the same fields from the underlying UCAN. It's true that you need the nnc
to make each invocation unique, and ext
is a nice place to extend the object, but it could be cut.
If you send a UCAN in a header to
POST https://example.com/rpc
, then we can infer the same information asrun: "*"
from the HTTP method. It's really only when you want to describe the direct interaction between objects in a transport agnostic way that signalling becomes useful.
Not that it matters, but we don't infer intent from HTTP method or endpoint. In fact ucanto can work over libp2p as well. In our case intent is inferred from the aud
of the delegation, if it is did:dns:web3.storage
we know we need to handle capabilities delegated in some way.
It's probably worth putting some info about intention signalling in general (over HTTP and other methods) in the core UCAN spec FAQ.
I did come around to the idea that signalling invocation explicitly is a good idea, even if in some cases intent may be clear, because there will be cases where it may not be so.
I keep coming back to the idea that we should keep additional fields out of the core UCAN token because it makes things like ucan-ipld much harder.
Why so ? I don't think it's necessarily harder to add new fields, but it does require updating schema if that's what you mean. That said prototyping in fct
seems like reasonable way forward to me as well, if things get widely adopted they could be promoted to dedicated fields. Prototyping with arbitrary fields has downside that if things get promoted to standard some UCANs will inevitably become incompatible because they used that field differently.
Adding invocation info to the core UCAN token is a one-way door. Once it's in there, we have to account for it for a long time (as we've seen with changes to UCAN previously it's always painful).
Since IPVM has a really clear use case for promise pipelining and all that entails, we need some re-invocation signalling either way. If we later decide it's a bad idea, it's totally fine: nothing is baked in at the core UCAN layer, so we just stop using them. It's a totally separate object. If we find that it's really useful and want to avoid the extra wrapper in the simple case, we can add that later. It's much harder to go the other way around.
This is a great argument to stick to use envelope! We could try various competing envelopes without coordination costs 💯
After thinking about it some more, this is actually probably not how I'd model this. It directly mixes concerns of authority with execution, which I think need to be extra clear.
I would like to know more! It does feel however that it's probably to have this conversation further in the future after we both had chance to evaluate things in practice.
You totally can do this (and in fact no one can stop you, because UCANs are extensible), but special first-class with and can in UCAN have not been popular in the past (looking at you my:// and ucan/delegate).
The thing is I don't want those to be a first-class or in any way superior to other capabilities. That is also a reason why I'm hesitant to making orchestration special, I really want those to be primitives that runtime could expose though capabilities. Anyway let's put a pin into this, we can revisit this once I have some more napkin sketch. 📌
Some minor questions about Receipts, and what the end state of VarSig will be, but I think this is getting really close!
So, wild idea that I tried out earlier and then reverted, but my mind keeps coming back to:
In UCAN v0.10, we're going to drop the prf
field so that you can construct a graph out of whatever proofs you happen to have, not what was in the chain at the time / what was delegated to you.
What if we did the same for invocations?
type SignedInvocation struct {
inv Invocation (rename "ucan/invoke")
prf [&UCAN] (implicit []) -- New optional field, unsigned since they're already signed
sig VarSig
}
type Invocation struct {
v SemVer -- Version
-- prf [&UCAN] <-- DROP THIS FIELD
run Scope -- Which tasks to invoke
nnc String -- Nonce
ext nullable Any (implicit null) -- Extended fields
}
type Scope union {
| All [&UCAN] -- Put UCANs here if needed
| Specific {String : Task}
}
{
"ucan/invoke": {
"v": "0.1.0",
"nnc": "02468",
"ext": null,
"run": [ // <-- HERE! INNER LEVEL
{"/": "bafkreie2cyfsaqv5jjy2gadr7mmupmearkvcg7llybfdd7b6fvzzmhazuy"},
{"/": "bafkreibbz5pksvfjyima4x4mduqpmvql2l4gh5afaj4ktmw6rwompxynx4"}
]
},
"sig": {"/": {"bytes:": "5vNn4--uTeGk_vayyPuNTYJ71Yr2nWkc6AkTv1QPWSgetpsu8SHegWoDakPVTdxkWb6nhVKAz6JdpgnjABppC7"}}
}
{
"ucan/invoke": {
"v": "0.1.0",
"nnc": "02468",
"ext": null,
"run": {
"notify-bob": {
"with": "mailto://alice@example.com",
"do": "msg/send",
"inputs": [
{
"to": "bob@example.com",
"subject": "DNSLink for example.com",
"body": "Hello Bob!"
}
]
},
"log-as-done": {
"with": "https://example.com/report"
"do": "crud/update"
"inputs": {
"from": "mailto://alice@exmaple.com",
"to": ["bob@exmaple.com"],
"event": "email-notification",
"value": {"ucan/promise": ["/", "notify-bob"]}
}
}
}
},
"prf": [ // <-- HERE! OUTER LEVEL
{"/": "bafkreie2cyfsaqv5jjy2gadr7mmupmearkvcg7llybfdd7b6fvzzmhazuy"},
{"/": "bafkreibbz5pksvfjyima4x4mduqpmvql2l4gh5afaj4ktmw6rwompxynx4"}
],
"sig": {"/": {"bytes:": "5vNn4--uTeGk_vayyPuNTYJ71Yr2nWkc6AkTv1QPWSgetpsu8SHegWoDakPVTdxkWb6nhVKAz6JdpgnjABppC7"}}
}
Here the prf
field in the outer wrapper doesn't get signed since they're already signed (because they're UCANs). That field is there "just" to be a way for them to piggyback on the request, but you could also not use that field at all or change it if you later discover that one was revoked or something.
It does take CIDs if you're invoking "all", because you need to say what you're invoking. I reverted it earlier because now there's two possible places for a UCAN to go: in the run
field, or in the prf
field (and/or out of band, but that last part is always true in UCAN 0.10)
In UCAN v0.10, we're going to drop the
prf
field so that you can construct a graph out of whatever proofs you happen to have, not what was in the chain at the time / what was delegated to you.
We're dropping prf
field ? Where are we going to stick the proofs then ?
We're dropping prf field ? Where are we going to stick the proofs then ?
Maybe I need to back up: my understanding of where we got to with https://github.com/ucan-wg/spec/issues/130 was that you could do things like this:
UCAN_A UCAN_D
/ \ /
V V V
UCAN_B UCAN_W
\ /
V V
UCAN_C UCAN_E
/ \ /
V V V
UCAN_X UCAN_Y
\ /
V V
UCAN_Z
...and substitute any UCAN as a proof as long as it matches the scoping and principal alignment rules. If I have UCAN_Z, I could give someone any of the 7 possible proof chains that make this up. In order to have that freedom, we need to pull the CIDs out of the signed UCAN payload, otherwise we limit ourselves to whatever is in the payload.
Is that not your current picture, @Gozala?
Where are we going to stick the proofs then ?
We'd probably put them in the CAR file or HTTP headers, and let the recipient figure out how to construct the graph. This is what I was saying the other day about it increasing the amount of work for them, but ultimately it's not THAT different from what they do today. The difference is that it happens on a per-transport basis, which is already the case with CIDs in the prf
field.
Oh! If you meant for the invocation, then they'd go here instead of run: "*"
:
"run": [
{"/": "bafkreie2cyfsaqv5jjy2gadr7mmupmearkvcg7llybfdd7b6fvzzmhazuy"},
{"/": "bafkreibbz5pksvfjyima4x4mduqpmvql2l4gh5afaj4ktmw6rwompxynx4"}
]
we need to pull the CIDs out of the signed UCAN payload, otherwise we limit ourselves to whatever is in the payload.
Oh I see you mean not remove the field, but rather remove it from the signature payload
To be honest, I was not proposing that just allowing to stick proofs that aren't principal aligned. That way they could be utilized down the chain. That said I think removing it from the signature is intriguing and I think it makes sense.
I think removing it from the signature is intriguing and I think it makes sense.
Awesome 🎉 Also FWIW, when I explained this as context for some other design decisions yesterday to @QuinnWilton, she was surprised that it didn't already work like that (it's how it worked in her head as someone who's heard about UCAN but never used them in production)
We'd probably put them in the CAR file or HTTP headers, and let the recipient figure out how to construct the graph.
I would really prefer to keep them inside UCAN, even if omitted from the signature. That way there is information about graph roots.
@Gozala let's move this back to the Issue on the core spec
@expede I just had this idea, which may be silly but maybe there is something to it.
Most of the promise pipelining syntax deals with naming individual task invocations so that they can be referenced with-in the other task invocations. Perhaps we don't need that, because we could just address task invocation by it's CID naturally making it possible to reference it from other tasks. It undoubtedly will look ugly in JSON, but in the language domain you could simply create task objects and then refer to them in other tasks serializing bunch of them into single invocation
Perhaps we don't need that, because we could just address task invocation by it's CID naturally making it possible to reference it from other tasks.
You're right that naming makes it easier for human readers. It also supports CIDs. This is akin to a CID vs a path under a CID, but we again a lot of legibility.
It undoubtedly will look ugly in JSON, but in the language domain you could simply create task objects and then refer to them in other tasks serializing bunch of them into single invocation
Though I should also back up: yes, this is the idea with pipelining! You should be able to call them inside the same invocation, or across several separate invocations 💯
You're right that naming makes it easier for human readers. It also supports CIDs. This is akin to a CID vs a path under a CID, but we again a lot of legibility.
I personally think this is not the right layer for the sugar & would much rather keep the spec simple.
On the related subject remember in Lisbon I really wanted to have separate notation for the "application". I thin promises here are an excellent example of why I wanted them, so you could reference result of the application as opposed to application itself.
I personally think this is not the right layer for the sugar & would much rather keep the spec simple.
Totally valid! I've been thinking of it as paths, which aren't really sugar per se, right? It's still all content addressed data, just like how a path under a CID is content addressed.
Is is that there's now more than one way to represent the path when you use the implicit "*"
but need to point at a specific capability in the underlying UCAN?
I've been thinking of it as paths, which aren't really sugar per se, right? It's still all content addressed data, just like how a path under a CID is content addressed.
What I mean is it increases the spec surface / scope for very little benefit as far as I can tell. Most of this will be in CBOR anyway so addressing things by CID vs local paths not going to matter.
It is also something that can be added in the future iteration if addressing by CID proves to be too constrained.
Is is that there's now more than one way to represent the path when you use the implicit "*" but need to point at a specific capability in the underlying UCAN?
I don't understand the question, mind rephrasing or elaborating a bit more ?
Most of this will be in CBOR anyway so addressing things by CID vs local paths not going to matter.
It really depends: it's IPLD, so you should also be able to make this DAG-JSON or DAG-PB 🤷♀️ Readability matters for debugging at minimum. Spec surface is a good point, though. I had that in a previous version, but changed it because it was hard trace through what was happening. Let me think on it some more, though.
I don't understand the question, mind rephrasing or elaborating a bit more ?
Sure!
This is only a problem in the case where you don't have a label for a particular action. We always want to know that both the specific invocation and which action inside it ran. This is because you can run the same task multiple times over the course of years, mnay different agents asking for it to be run etc. All of that info (currently) resides on the outer envelope, which seems like the right place for it.
This means that we're always addressing an action relative to the envelope. So in the named case, this is roughly equivalent to an IPLD path: /bafySpecificInvocation/run/my-task
. It's not the resolved path, though. It's always the tuple (invocation, action)
, because the invocation gives you the uniqueness. We can also get this with something like sha(invocation + action)
but that's actually harder to work with because now you have to track all of these arbitrary hashes. Hence: ["bafyInvocation", "task-name"]
. If we used the CID of the task, we'd need some way to distinguish between identical tasks.
In this diagram, I'm using the syntax label:taskContent
as a shorthand, where taskB
looks identical, but we need two of them with the same input.
begin:taskA -> left:taskB
|
V
down:taskB
You don't have this problem in the simple run: "*"
case, because UCAN v0.10 doesn't support multiple identical entries. They're all unique by definition, and thus can be indexed by their CID. They don't have a label, so in the current spec, we infer a label from their CID.
@Gozala especially given your use cases, I'm increasingly in favour of breaking this spec up. It really comes from IPVM work, but I think DAG House has a simpler model. Before I actually go and put a bunch of work into this, here's a proposed ontology which changes a bunch of the wording around:
Concept | Spec | Description |
---|---|---|
"Invocation" | ucan-wg/invocation |
Single "function invocation" |
"Pointer" AND "Receipt" | ucan-wg/receipt |
Receipt of a single invocation |
"Batch" | ucan-wg/batch |
Several invocations together |
"Promise" | ucan-wg/promise |
Message that says "follow that previous thing with this invocation" |
"Pipeline" | ucan-wg/pipeline |
Pipelined invocations in a batch |
On one hand, this is pretty granular. On the other, the current spec absolutely mixes a bunch of concerns and may limit the reuse of components.
Strictly speaking, batching could be separate from promises, too. There's a question about how granular this should be. Here's the granular dependencies:
Here's the granular dependencies:
Okay yeah, at minimum I'm going to reorganize this spec according to the diagram, and make fix the naming to be consistent with the above.
Attempting the above-mentioned reorg for improved concept clarity, and potential to break the spec up: https://github.com/ucan-wg/invocation/pull/4
All of that info (currently) resides on the outer envelope, which seems like the right place for it.
ah right I get it now! I’m glad we’re talking about this because this was something that we’ve considered in our invocation logic and made some decisions that look different:
Before I dive in lets make sure we agree on terms because we keep tripping over using conflicting terms in our work.
task - in ucanto we don’t have tasks we have invocation which is represented as delegation with a single capability. All the inputs are part of nb
and if they are large they are just CIDs, actual data CID points to is packed in the same CAR (there some cases when it’s not bundled intentionally but I’m not going to go into that)
query - we wanted to bundle bunch of invocations and execute those with one request graphql style. Query syntax and selectors got complicated and we needed to cut corners. So we settled on primitive queries for now, which essentially a tuple of invocation links, which get tuple of corresponding results.
We don’t need special syntax for promises because in our case those are invocations and we can refer them by cid.
The reason I wrote all this is because I think choice of envelope really shaped out some tradeoffs. Specifically addressing things by IPLD path was something we intentionally tried to avoid so you could refer to whole thing without revealing anything about outer layers
Pulling the low-level capability invocation bits out of https://github.com/ipvm-wg/spec/pull/8 to UCAN because this layer doesn't have any direct IPVM dependencies