ucan-wg / invocation

UCAN Invocation & Pipelining
Other
12 stars 5 forks source link

v0.1.0 #1

Closed expede closed 1 year ago

expede commented 1 year ago

📜 Preview

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

expede commented 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
expede commented 1 year ago

@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 commented 1 year ago

@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!

expede commented 1 year ago

😆 I'm having to break certain features across IPVM and UCAN. Organization is hard!

expede commented 1 year ago

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)

Gozala commented 1 year ago

@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.

Gozala commented 1 year ago

@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

expede commented 1 year ago

@Gozala

for an envelope as opposed to say extension of the UCAN model

There's really two:

  1. Pipelining
  2. Separation of concerns

Pipelining

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.

Separation of Concerns

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.

Timeouts vs Exact Once Semantics

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.

expede commented 1 year ago

...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

expede commented 1 year ago

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.

expede commented 1 year ago

☝️ 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.

expede commented 1 year ago

☝️ (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)

Gozala commented 1 year ago

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.

  1. One less signature to create and validate.
  2. I just fear added complexity. I already get ppl complaining about all the complexity & somehow added field, which in simple case could have value “*”, seems a lot simpler to explain than envelop with bunch of similar fields as UCAN.
expede commented 1 year ago

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:

A 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 commented 1 year ago

@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.

Gozala commented 1 year ago

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

  1. You can not conceal invocation details if you want to (re)delegate some or all capabilities out to another agent.
  2. When you want to re-invoke you can’t link to same delegation chain.

    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

expede commented 1 year ago

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.

expede commented 1 year ago

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:

  1. An invocation wrapper (current proposal)
  2. A nested invocation object (I found this hard to work with in an earlier draft, but I've been wrong before)
  3. An invoke: true flag, and add promises etc throughout core UCANs (I don't like this one)
Gozala commented 1 year ago

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: { … }
}
Gozala commented 1 year ago

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

expede commented 1 year ago

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.

Wild Idea

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?

expede commented 1 year ago

(Perhaps for context: we expect re-invocaton to be quite common in IPVM)

expede commented 1 year ago

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" 🤣

expede commented 1 year ago

@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

Gozala commented 1 year ago

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:

  1. Use att: [{ "with": "ucan:*", "can": "*" }] implying do everything in the prf.
  2. Consider omitting att when invoke is set, to imply all the caps in the prf.
Gozala commented 1 year ago

@expede here is the summary from our conversation earlier today:

  1. I think taking run field from Invocation model and adding it as optional invoke to UCAN would have following benefits
    • No more need to repeat iss, aud, nnc, ext, fields.
    • No need for another signature.
    • All the above just reuses UCANs and this spec can focus only on what goes into run field.
    • Executor can still (re)delegate capabilities simply just wrapping with another UCAN, because ucan with optional invoke field is still a UCAN.
  2. It may be worth considering factoring out orchestration syntax
    • E.g. 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.
    • It would also imply that some of task scheduling and pipelining could be exposed using other system capabilities to runtime, so it MAY do it dynamically as long as it has capabilities to do so.
    • Above is similar to how solana does cross-program invocations.
    • Also note that invoked program may not be able to call into other program, because it may not be delegated system/execute capability. Or it could be given limited program calling capability by restricting programs it may call.
expede commented 1 year ago

@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:

Let A Thousand RPCs Bloom 🌸

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.

Facts Field Prototyping

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!

Avoiding One-Way Doors ⛔

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.

Separation of Concerns ✂️

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.

Factoring Out Orchestration

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).

Object Capabilities 🎈

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 💯

Minor Correction

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.

Gozala commented 1 year ago

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.

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. 📌

expede commented 1 year ago

Some minor questions about Receipts, and what the end state of VarSig will be, but I think this is getting really close!

expede commented 1 year ago

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)

Gozala commented 1 year ago

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 ?

expede commented 1 year ago

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?

expede commented 1 year ago

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.

expede commented 1 year ago

Oh! If you meant for the invocation, then they'd go here instead of run: "*":

    "run": [
      {"/": "bafkreie2cyfsaqv5jjy2gadr7mmupmearkvcg7llybfdd7b6fvzzmhazuy"},
      {"/": "bafkreibbz5pksvfjyima4x4mduqpmvql2l4gh5afaj4ktmw6rwompxynx4"}
    ]
Gozala commented 1 year ago

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.

expede commented 1 year ago

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)

Gozala commented 1 year ago

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.

expede commented 1 year ago

@Gozala let's move this back to the Issue on the core spec

Gozala commented 1 year ago

@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

expede commented 1 year ago

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.

expede commented 1 year ago

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 💯

Gozala commented 1 year ago

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.

expede commented 1 year ago

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?

Gozala commented 1 year ago

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 ?

expede commented 1 year ago

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.

expede commented 1 year ago

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.

expede commented 1 year ago

@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:

foo

expede commented 1 year ago

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.

expede commented 1 year ago

Attempting the above-mentioned reorg for improved concept clarity, and potential to break the spec up: https://github.com/ucan-wg/invocation/pull/4

Gozala commented 1 year ago

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